From d1454b5e8f051b00edb7b7e883b5530ea7ea4f72 Mon Sep 17 00:00:00 2001 From: Charles d'Avernas Date: Wed, 19 Jun 2024 13:21:23 +0200 Subject: [PATCH] feat(Dashboard): Added the WorkflowDiagram component (WIP) Signed-off-by: Charles d'Avernas --- .../Synapse.Api.Http/Synapse.Api.Http.csproj | 4 +- .../Namespaces/DeleteNamespaceCommand.cs | 3 +- .../Synapse.Core.Infrastructure.csproj | 10 +- .../WorkflowDefinitionExtensions.cs | 40 ++ src/core/Synapse.Core/Synapse.Core.csproj | 4 +- .../Synapse.Correlator.csproj | 12 +- .../WorkflowDetails/WorkflowDetails.razor | 9 + .../WorkflowDiagram/CallTaskNodeViewModel.cs | 34 ++ .../CompositeTaskNodeViewModel.cs | 34 ++ .../WorkflowDiagram/EmitTaskNodeViewModel.cs | 35 ++ .../WorkflowDiagram/EndNodeViewModel.cs | 27 ++ .../ExtensionTaskNodeViewModel.cs | 34 ++ .../WorkflowDiagram/ForTaskNodeViewModel.cs | 35 ++ .../WorkflowDiagram/IWorkflowNodeViewModel.cs | 39 ++ .../ListenTaskNodeViewModel.cs | 34 ++ .../WorkflowDiagram/RaiseTaskNodeViewModel.cs | 35 ++ .../WorkflowDiagram/RunTaskNodeViewModel.cs | 35 ++ .../WorkflowDiagram/SetTaskNodeViewModel.cs | 34 ++ .../WorkflowDiagram/StartNodeViewModel.cs | 30 ++ .../SwitchTaskNodeViewModel.cs | 35 ++ .../WorkflowDiagram/TaskNodeViewModel.cs | 83 ++++ .../Templates/NodeLabelTemplate.razor | 37 ++ .../Templates/NodeShapeTemplate.razor | 22 + .../Templates/TaskNodeTemplate.razor | 23 + .../Templates/WorkflowNodeTemplate.razor | 24 + .../WorkflowDiagram/TryTaskNodeViewModel.cs | 35 ++ .../WorkflowDiagram/WaitTaskNodeViewModel.cs | 35 ++ .../WorkflowDiagram/WorkflowDiagram.razor | 59 +++ .../WorkflowDiagramOrientation.cs | 29 ++ .../Pages/Authentication/Bearer/Login.razor | 2 +- .../Pages/Workflows/List/View.razor | 10 + src/dashboard/Synapse.Dashboard/Program.cs | 4 +- .../Interfaces/ISecurityTokenManager.cs | 2 +- .../Interfaces/IWorkflowGraphBuilder.cs | 32 ++ .../Services/LabeledWorkflowNodeViewModel.cs | 34 ++ .../Services/WorkflowGraphBuilder.cs | 443 ++++++++++++++++++ .../Services/WorkflowNodeViewModel.cs | 67 +++ .../Synapse.Dashboard.csproj | 1 + .../Synapse.Dashboard/_Imports.razor | 3 + .../Synapse.Dashboard/wwwroot/index.html | 6 + .../Services/WorkflowExecutionContext.cs | 94 ++-- .../Synapse.Runner/Synapse.Runner.csproj | 10 +- .../Synapse.IntegrationTests.csproj | 2 +- .../Synapse.UnitTests.csproj | 8 +- 44 files changed, 1507 insertions(+), 81 deletions(-) create mode 100644 src/core/Synapse.Core/Extensions/WorkflowDefinitionExtensions.cs create mode 100644 src/dashboard/Synapse.Dashboard/Components/WorkflowDetails/WorkflowDetails.razor create mode 100644 src/dashboard/Synapse.Dashboard/Components/WorkflowDiagram/CallTaskNodeViewModel.cs create mode 100644 src/dashboard/Synapse.Dashboard/Components/WorkflowDiagram/CompositeTaskNodeViewModel.cs create mode 100644 src/dashboard/Synapse.Dashboard/Components/WorkflowDiagram/EmitTaskNodeViewModel.cs create mode 100644 src/dashboard/Synapse.Dashboard/Components/WorkflowDiagram/EndNodeViewModel.cs create mode 100644 src/dashboard/Synapse.Dashboard/Components/WorkflowDiagram/ExtensionTaskNodeViewModel.cs create mode 100644 src/dashboard/Synapse.Dashboard/Components/WorkflowDiagram/ForTaskNodeViewModel.cs create mode 100644 src/dashboard/Synapse.Dashboard/Components/WorkflowDiagram/IWorkflowNodeViewModel.cs create mode 100644 src/dashboard/Synapse.Dashboard/Components/WorkflowDiagram/ListenTaskNodeViewModel.cs create mode 100644 src/dashboard/Synapse.Dashboard/Components/WorkflowDiagram/RaiseTaskNodeViewModel.cs create mode 100644 src/dashboard/Synapse.Dashboard/Components/WorkflowDiagram/RunTaskNodeViewModel.cs create mode 100644 src/dashboard/Synapse.Dashboard/Components/WorkflowDiagram/SetTaskNodeViewModel.cs create mode 100644 src/dashboard/Synapse.Dashboard/Components/WorkflowDiagram/StartNodeViewModel.cs create mode 100644 src/dashboard/Synapse.Dashboard/Components/WorkflowDiagram/SwitchTaskNodeViewModel.cs create mode 100644 src/dashboard/Synapse.Dashboard/Components/WorkflowDiagram/TaskNodeViewModel.cs create mode 100644 src/dashboard/Synapse.Dashboard/Components/WorkflowDiagram/Templates/NodeLabelTemplate.razor create mode 100644 src/dashboard/Synapse.Dashboard/Components/WorkflowDiagram/Templates/NodeShapeTemplate.razor create mode 100644 src/dashboard/Synapse.Dashboard/Components/WorkflowDiagram/Templates/TaskNodeTemplate.razor create mode 100644 src/dashboard/Synapse.Dashboard/Components/WorkflowDiagram/Templates/WorkflowNodeTemplate.razor create mode 100644 src/dashboard/Synapse.Dashboard/Components/WorkflowDiagram/TryTaskNodeViewModel.cs create mode 100644 src/dashboard/Synapse.Dashboard/Components/WorkflowDiagram/WaitTaskNodeViewModel.cs create mode 100644 src/dashboard/Synapse.Dashboard/Components/WorkflowDiagram/WorkflowDiagram.razor create mode 100644 src/dashboard/Synapse.Dashboard/Components/WorkflowDiagram/WorkflowDiagramOrientation.cs create mode 100644 src/dashboard/Synapse.Dashboard/Services/Interfaces/IWorkflowGraphBuilder.cs create mode 100644 src/dashboard/Synapse.Dashboard/Services/LabeledWorkflowNodeViewModel.cs create mode 100644 src/dashboard/Synapse.Dashboard/Services/WorkflowGraphBuilder.cs create mode 100644 src/dashboard/Synapse.Dashboard/Services/WorkflowNodeViewModel.cs diff --git a/src/api/Synapse.Api.Http/Synapse.Api.Http.csproj b/src/api/Synapse.Api.Http/Synapse.Api.Http.csproj index 96fa8ead1..6523e22e4 100644 --- a/src/api/Synapse.Api.Http/Synapse.Api.Http.csproj +++ b/src/api/Synapse.Api.Http/Synapse.Api.Http.csproj @@ -10,8 +10,8 @@ - - + + diff --git a/src/cli/Synapse.Cli/Commands/Namespaces/DeleteNamespaceCommand.cs b/src/cli/Synapse.Cli/Commands/Namespaces/DeleteNamespaceCommand.cs index aafa3b8c2..eec7c4517 100644 --- a/src/cli/Synapse.Cli/Commands/Namespaces/DeleteNamespaceCommand.cs +++ b/src/cli/Synapse.Cli/Commands/Namespaces/DeleteNamespaceCommand.cs @@ -54,11 +54,10 @@ public async Task HandleAsync(string name, bool y) { Console.Write($"Are you sure you wish to delete the namespace '{name}'? Press 'y' to confirm, or any other key to cancel: "); var inputKey = Console.ReadKey(); - Console.WriteLine(); if (inputKey.Key != ConsoleKey.Y) return; } await this.Api.Namespaces.DeleteAsync(name); - Console.WriteLine($"namespace/{name} deleted"); + Console.Write($"namespace/{name} deleted"); } static class CommandOptions diff --git a/src/core/Synapse.Core.Infrastructure/Synapse.Core.Infrastructure.csproj b/src/core/Synapse.Core.Infrastructure/Synapse.Core.Infrastructure.csproj index c6960cfd6..cc1381fcb 100644 --- a/src/core/Synapse.Core.Infrastructure/Synapse.Core.Infrastructure.csproj +++ b/src/core/Synapse.Core.Infrastructure/Synapse.Core.Infrastructure.csproj @@ -10,11 +10,11 @@ - - - - - + + + + + diff --git a/src/core/Synapse.Core/Extensions/WorkflowDefinitionExtensions.cs b/src/core/Synapse.Core/Extensions/WorkflowDefinitionExtensions.cs new file mode 100644 index 000000000..800eb1510 --- /dev/null +++ b/src/core/Synapse.Core/Extensions/WorkflowDefinitionExtensions.cs @@ -0,0 +1,40 @@ +// Copyright © 2024-Present The Synapse Authors +// +// Licensed under the Apache License, Version 2.0 (the "License"), +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +namespace Synapse; + +/// +/// Defines extensions for s +/// +public static class WorkflowDefinitionExtensions +{ + + /// + /// Gets the task, if any, that should be executed after the specified one + /// + /// The that defines the specified + /// The name/definition mapping of the to get the following of + /// A reference to the component that defines the next + /// A name/definition mapping of the next , if any + public static KeyValuePair? GetNextTask(this WorkflowDefinition workflow, KeyValuePair afterTask, string parentReference) + { + ArgumentNullException.ThrowIfNull(workflow); + ArgumentException.ThrowIfNullOrWhiteSpace(parentReference); + var taskMap = workflow.GetComponent>(parentReference) ?? throw new NullReferenceException($"Failed to find the component at '{parentReference}'"); + var taskIndex = taskMap.Keys.ToList().IndexOf(afterTask.Key); + var nextIndex = taskIndex < 0 ? -1 : taskIndex + 1; + if (nextIndex < 0 || nextIndex >= taskMap.Count) return null; + return taskMap.ElementAt(nextIndex); + } + +} \ No newline at end of file diff --git a/src/core/Synapse.Core/Synapse.Core.csproj b/src/core/Synapse.Core/Synapse.Core.csproj index f6fb8776a..e82c6d2aa 100644 --- a/src/core/Synapse.Core/Synapse.Core.csproj +++ b/src/core/Synapse.Core/Synapse.Core.csproj @@ -32,8 +32,8 @@ - - + + diff --git a/src/correlator/Synapse.Correlator/Synapse.Correlator.csproj b/src/correlator/Synapse.Correlator/Synapse.Correlator.csproj index e1f4b2c16..fb8a7f0ea 100644 --- a/src/correlator/Synapse.Correlator/Synapse.Correlator.csproj +++ b/src/correlator/Synapse.Correlator/Synapse.Correlator.csproj @@ -16,12 +16,12 @@ - - - - - - + + + + + + diff --git a/src/dashboard/Synapse.Dashboard/Components/WorkflowDetails/WorkflowDetails.razor b/src/dashboard/Synapse.Dashboard/Components/WorkflowDetails/WorkflowDetails.razor new file mode 100644 index 000000000..d27dffca6 --- /dev/null +++ b/src/dashboard/Synapse.Dashboard/Components/WorkflowDetails/WorkflowDetails.razor @@ -0,0 +1,9 @@ +@namespace Synapse.Dashboard.Components + + + +@code{ + + [Parameter] public Workflow Workflow { get; set; } = null!; + +} \ No newline at end of file diff --git a/src/dashboard/Synapse.Dashboard/Components/WorkflowDiagram/CallTaskNodeViewModel.cs b/src/dashboard/Synapse.Dashboard/Components/WorkflowDiagram/CallTaskNodeViewModel.cs new file mode 100644 index 000000000..dff64f4d2 --- /dev/null +++ b/src/dashboard/Synapse.Dashboard/Components/WorkflowDiagram/CallTaskNodeViewModel.cs @@ -0,0 +1,34 @@ +// Copyright © 2024-Present The Synapse Authors +// +// Licensed under the Apache License, Version 2.0 (the "License"), +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +using ServerlessWorkflow.Sdk.Models.Tasks; + +namespace Synapse.Dashboard.Components; + +/// +/// Represents a call task node view model +/// +public class CallTaskNodeViewModel + : LabeledWorkflowNodeViewModel +{ + + /// + /// Initializes a new + /// + public CallTaskNodeViewModel(KeyValuePair task) + : base(task.Key, "call-task-node", null, Neuroglia.Blazor.Dagre.Constants.NodeHeight * 1.5, Neuroglia.Blazor.Dagre.Constants.NodeHeight * 1.5) + { + + } + +} diff --git a/src/dashboard/Synapse.Dashboard/Components/WorkflowDiagram/CompositeTaskNodeViewModel.cs b/src/dashboard/Synapse.Dashboard/Components/WorkflowDiagram/CompositeTaskNodeViewModel.cs new file mode 100644 index 000000000..fb85958a3 --- /dev/null +++ b/src/dashboard/Synapse.Dashboard/Components/WorkflowDiagram/CompositeTaskNodeViewModel.cs @@ -0,0 +1,34 @@ +// Copyright © 2024-Present The Synapse Authors +// +// Licensed under the Apache License, Version 2.0 (the "License"), +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +using ServerlessWorkflow.Sdk.Models.Tasks; + +namespace Synapse.Dashboard.Components; + +/// +/// Represents a composite task node view model +/// +public class CompositeTaskNodeViewModel + : LabeledWorkflowNodeViewModel +{ + + /// + /// Initializes a new + /// + public CompositeTaskNodeViewModel(KeyValuePair task) + : base(task.Key, "composite-task-node", null, Neuroglia.Blazor.Dagre.Constants.NodeHeight * 1.5, Neuroglia.Blazor.Dagre.Constants.NodeHeight * 1.5) + { + + } + +} diff --git a/src/dashboard/Synapse.Dashboard/Components/WorkflowDiagram/EmitTaskNodeViewModel.cs b/src/dashboard/Synapse.Dashboard/Components/WorkflowDiagram/EmitTaskNodeViewModel.cs new file mode 100644 index 000000000..7e17d220d --- /dev/null +++ b/src/dashboard/Synapse.Dashboard/Components/WorkflowDiagram/EmitTaskNodeViewModel.cs @@ -0,0 +1,35 @@ +// Copyright © 2024-Present The Synapse Authors +// +// Licensed under the Apache License, Version 2.0 (the "License"), +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +using Neuroglia.Blazor.Dagre; +using ServerlessWorkflow.Sdk.Models.Tasks; + +namespace Synapse.Dashboard.Components; + +/// +/// Represents an emit task node view model +/// +public class EmitTaskNodeViewModel + : LabeledWorkflowNodeViewModel +{ + + /// + /// Initializes a new + /// + public EmitTaskNodeViewModel(KeyValuePair task) + : base(task.Key, "emit-task-node", null, Constants.NodeHeight * 1.5, Constants.NodeHeight * 1.5) + { + + } + +} diff --git a/src/dashboard/Synapse.Dashboard/Components/WorkflowDiagram/EndNodeViewModel.cs b/src/dashboard/Synapse.Dashboard/Components/WorkflowDiagram/EndNodeViewModel.cs new file mode 100644 index 000000000..bd572ae3f --- /dev/null +++ b/src/dashboard/Synapse.Dashboard/Components/WorkflowDiagram/EndNodeViewModel.cs @@ -0,0 +1,27 @@ +// Copyright © 2024-Present The Synapse Authors +// +// Licensed under the Apache License, Version 2.0 (the "License"), +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +using Neuroglia.Blazor.Dagre; + +namespace Synapse.Dashboard.Components; + +/// +/// Represents the object that holds the data required to render the view of a workflow's end node +/// +public class EndNodeViewModel() + : WorkflowNodeViewModel(string.Empty, "end-node", NodeShape.Circle, WorkflowGraphBuilder.StartEndNodeRadius, WorkflowGraphBuilder.StartEndNodeRadius) +{ + + + +} diff --git a/src/dashboard/Synapse.Dashboard/Components/WorkflowDiagram/ExtensionTaskNodeViewModel.cs b/src/dashboard/Synapse.Dashboard/Components/WorkflowDiagram/ExtensionTaskNodeViewModel.cs new file mode 100644 index 000000000..0f5d6a96a --- /dev/null +++ b/src/dashboard/Synapse.Dashboard/Components/WorkflowDiagram/ExtensionTaskNodeViewModel.cs @@ -0,0 +1,34 @@ +// Copyright © 2024-Present The Synapse Authors +// +// Licensed under the Apache License, Version 2.0 (the "License"), +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +using ServerlessWorkflow.Sdk.Models.Tasks; + +namespace Synapse.Dashboard.Components; + +/// +/// Represents a extension task node view model +/// +public class ExtensionTaskNodeViewModel + : LabeledWorkflowNodeViewModel +{ + + /// + /// Initializes a new + /// + public ExtensionTaskNodeViewModel(KeyValuePair task) + : base(task.Key, "composite-task-node", null, Neuroglia.Blazor.Dagre.Constants.NodeHeight * 1.5, Neuroglia.Blazor.Dagre.Constants.NodeHeight * 1.5) + { + + } + +} \ No newline at end of file diff --git a/src/dashboard/Synapse.Dashboard/Components/WorkflowDiagram/ForTaskNodeViewModel.cs b/src/dashboard/Synapse.Dashboard/Components/WorkflowDiagram/ForTaskNodeViewModel.cs new file mode 100644 index 000000000..e1db2889d --- /dev/null +++ b/src/dashboard/Synapse.Dashboard/Components/WorkflowDiagram/ForTaskNodeViewModel.cs @@ -0,0 +1,35 @@ +// Copyright © 2024-Present The Synapse Authors +// +// Licensed under the Apache License, Version 2.0 (the "License"), +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +using Neuroglia.Blazor.Dagre; +using ServerlessWorkflow.Sdk.Models.Tasks; + +namespace Synapse.Dashboard.Components; + +/// +/// Represents a for task node view model +/// +public class ForTaskNodeViewModel + : LabeledWorkflowNodeViewModel +{ + + /// + /// Initializes a new + /// + public ForTaskNodeViewModel(KeyValuePair task) + : base(task.Key, "for-task-node", null, Constants.NodeHeight * 1.5, Constants.NodeHeight * 1.5) + { + + } + +} diff --git a/src/dashboard/Synapse.Dashboard/Components/WorkflowDiagram/IWorkflowNodeViewModel.cs b/src/dashboard/Synapse.Dashboard/Components/WorkflowDiagram/IWorkflowNodeViewModel.cs new file mode 100644 index 000000000..b27dd3cd0 --- /dev/null +++ b/src/dashboard/Synapse.Dashboard/Components/WorkflowDiagram/IWorkflowNodeViewModel.cs @@ -0,0 +1,39 @@ +// Copyright © 2024-Present The Synapse Authors +// +// Licensed under the Apache License, Version 2.0 (the "License"), +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +using Synapse.Resources; + +namespace Synapse.Dashboard.Components; + +/// +/// Defines the fundamentals of a workflow node +/// +public interface IWorkflowNodeViewModel +{ + + /// + /// Gets/Sets the number of active s for which the task described by the node is operative + /// + int OperativeInstancesCount { get; set; } + + /// + /// Gets/Sets the number of active faulted s for which the task described by the node is faulted + /// + int FaultedInstancesCount { get; set; } + + /// + /// Resets the operative and faulted instances counts + /// + void ResetInstancesCount(); + +} diff --git a/src/dashboard/Synapse.Dashboard/Components/WorkflowDiagram/ListenTaskNodeViewModel.cs b/src/dashboard/Synapse.Dashboard/Components/WorkflowDiagram/ListenTaskNodeViewModel.cs new file mode 100644 index 000000000..d2df22d79 --- /dev/null +++ b/src/dashboard/Synapse.Dashboard/Components/WorkflowDiagram/ListenTaskNodeViewModel.cs @@ -0,0 +1,34 @@ +// Copyright © 2024-Present The Synapse Authors +// +// Licensed under the Apache License, Version 2.0 (the "License"), +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +using ServerlessWorkflow.Sdk.Models.Tasks; + +namespace Synapse.Dashboard.Components; + +/// +/// Represents a listen task node view model +/// +public class ListenTaskNodeViewModel + : LabeledWorkflowNodeViewModel +{ + + /// + /// Initializes a new + /// + public ListenTaskNodeViewModel(KeyValuePair task) + : base(task.Key, "listen-task-node", null, Neuroglia.Blazor.Dagre.Constants.NodeHeight * 1.5, Neuroglia.Blazor.Dagre.Constants.NodeHeight * 1.5) + { + + } + +} diff --git a/src/dashboard/Synapse.Dashboard/Components/WorkflowDiagram/RaiseTaskNodeViewModel.cs b/src/dashboard/Synapse.Dashboard/Components/WorkflowDiagram/RaiseTaskNodeViewModel.cs new file mode 100644 index 000000000..f6be995b9 --- /dev/null +++ b/src/dashboard/Synapse.Dashboard/Components/WorkflowDiagram/RaiseTaskNodeViewModel.cs @@ -0,0 +1,35 @@ +// Copyright © 2024-Present The Synapse Authors +// +// Licensed under the Apache License, Version 2.0 (the "License"), +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +using Neuroglia.Blazor.Dagre; +using ServerlessWorkflow.Sdk.Models.Tasks; + +namespace Synapse.Dashboard.Components; + +/// +/// Represents a raise task node view model +/// +public class RaiseTaskNodeViewModel + : LabeledWorkflowNodeViewModel +{ + + /// + /// Initializes a new + /// + public RaiseTaskNodeViewModel(KeyValuePair task) + : base(task.Key, "raise-task-node", null, Constants.NodeHeight * 1.5, Constants.NodeHeight * 1.5) + { + + } + +} diff --git a/src/dashboard/Synapse.Dashboard/Components/WorkflowDiagram/RunTaskNodeViewModel.cs b/src/dashboard/Synapse.Dashboard/Components/WorkflowDiagram/RunTaskNodeViewModel.cs new file mode 100644 index 000000000..8f2c1c230 --- /dev/null +++ b/src/dashboard/Synapse.Dashboard/Components/WorkflowDiagram/RunTaskNodeViewModel.cs @@ -0,0 +1,35 @@ +// Copyright © 2024-Present The Synapse Authors +// +// Licensed under the Apache License, Version 2.0 (the "License"), +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +using Neuroglia.Blazor.Dagre; +using ServerlessWorkflow.Sdk.Models.Tasks; + +namespace Synapse.Dashboard.Components; + +/// +/// Represents a run task node view model +/// +public class RunTaskNodeViewModel + : LabeledWorkflowNodeViewModel +{ + + /// + /// Initializes a new + /// + public RunTaskNodeViewModel(KeyValuePair task) + : base(task.Key, "run-task-node", null, Constants.NodeHeight * 1.5, Constants.NodeHeight * 1.5) + { + + } + +} diff --git a/src/dashboard/Synapse.Dashboard/Components/WorkflowDiagram/SetTaskNodeViewModel.cs b/src/dashboard/Synapse.Dashboard/Components/WorkflowDiagram/SetTaskNodeViewModel.cs new file mode 100644 index 000000000..8aaf15aff --- /dev/null +++ b/src/dashboard/Synapse.Dashboard/Components/WorkflowDiagram/SetTaskNodeViewModel.cs @@ -0,0 +1,34 @@ +// Copyright © 2024-Present The Synapse Authors +// +// Licensed under the Apache License, Version 2.0 (the "License"), +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +using ServerlessWorkflow.Sdk.Models.Tasks; + +namespace Synapse.Dashboard.Components; + +/// +/// Represents a set task node view model +/// +public class SetTaskNodeViewModel + : LabeledWorkflowNodeViewModel +{ + + /// + /// Initializes a new + /// + public SetTaskNodeViewModel(KeyValuePair task) + : base(task.Key, "set-task-node", null, Neuroglia.Blazor.Dagre.Constants.NodeHeight * 1.5, Neuroglia.Blazor.Dagre.Constants.NodeHeight * 1.5) + { + + } + +} diff --git a/src/dashboard/Synapse.Dashboard/Components/WorkflowDiagram/StartNodeViewModel.cs b/src/dashboard/Synapse.Dashboard/Components/WorkflowDiagram/StartNodeViewModel.cs new file mode 100644 index 000000000..39a4f9d49 --- /dev/null +++ b/src/dashboard/Synapse.Dashboard/Components/WorkflowDiagram/StartNodeViewModel.cs @@ -0,0 +1,30 @@ +// Copyright © 2024-Present The Synapse Authors +// +// Licensed under the Apache License, Version 2.0 (the "License"), +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +using Neuroglia.Blazor.Dagre; + +namespace Synapse.Dashboard.Components; + +/// +/// Represents the object that holds the data required to render the view of a workflow's start node +/// +public class StartNodeViewModel(bool hasSuccessor = false) + : WorkflowNodeViewModel(string.Empty, "start-node", NodeShape.Circle, WorkflowGraphBuilder.StartEndNodeRadius, WorkflowGraphBuilder.StartEndNodeRadius) +{ + + /// + /// Gets a boolean indicating whether or not the node has a successor + /// + public bool HasSuccessor { get; } = hasSuccessor; + +} \ No newline at end of file diff --git a/src/dashboard/Synapse.Dashboard/Components/WorkflowDiagram/SwitchTaskNodeViewModel.cs b/src/dashboard/Synapse.Dashboard/Components/WorkflowDiagram/SwitchTaskNodeViewModel.cs new file mode 100644 index 000000000..b209e8c2c --- /dev/null +++ b/src/dashboard/Synapse.Dashboard/Components/WorkflowDiagram/SwitchTaskNodeViewModel.cs @@ -0,0 +1,35 @@ +// Copyright © 2024-Present The Synapse Authors +// +// Licensed under the Apache License, Version 2.0 (the "License"), +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +using Neuroglia.Blazor.Dagre; +using ServerlessWorkflow.Sdk.Models.Tasks; + +namespace Synapse.Dashboard.Components; + +/// +/// Represents a switch task node view model +/// +public class SwitchTaskNodeViewModel + : LabeledWorkflowNodeViewModel +{ + + /// + /// Initializes a new + /// + public SwitchTaskNodeViewModel(KeyValuePair task) + : base(task.Key, "switch-task-node", null, Constants.NodeHeight * 1.5, Constants.NodeHeight * 1.5) + { + + } + +} diff --git a/src/dashboard/Synapse.Dashboard/Components/WorkflowDiagram/TaskNodeViewModel.cs b/src/dashboard/Synapse.Dashboard/Components/WorkflowDiagram/TaskNodeViewModel.cs new file mode 100644 index 000000000..3ed7b8d5a --- /dev/null +++ b/src/dashboard/Synapse.Dashboard/Components/WorkflowDiagram/TaskNodeViewModel.cs @@ -0,0 +1,83 @@ +// Copyright © 2024-Present The Synapse Authors +// +// Licensed under the Apache License, Version 2.0 (the "License"), +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +using Neuroglia.Blazor.Dagre.Models; +using ServerlessWorkflow.Sdk.Models; +using System.Text.Json.Serialization; + +namespace Synapse.Dashboard.Components; + +/// +/// Represents a +/// +public class TaskNodeViewModel + : ClusterViewModel, IWorkflowNodeViewModel +{ + + /// + /// Initializes a new + /// + /// The name/definition mapping of the the represents + /// Indicates whether or not the task to create the for is the first task of the workflow it belongs to + public TaskNodeViewModel(KeyValuePair task, bool isFirst = false) + : base(null, task.Key) + { + this.Task = task; + this.IsFirst = isFirst; + this.ComponentType = typeof(TaskNodeTemplate); + } + + /// + /// Gets the name/definition mapping of the the represents + /// + [JsonIgnore(Condition = JsonIgnoreCondition.Always)] + public KeyValuePair Task { get; } + + /// + /// Gets if the state is the first of the workflow + /// + public bool IsFirst { get; } + + int _operativeInstances = 0; + /// + public int OperativeInstancesCount + { + get => this._operativeInstances; + set + { + this._operativeInstances = value; + this.OnChange(); + } + } + + int _faultedInstances = 0; + /// + public int FaultedInstancesCount + { + get => this._faultedInstances; + set + { + this._faultedInstances = value; + this.OnChange(); + } + } + + /// + public void ResetInstancesCount() + { + this._operativeInstances = 0; + this._faultedInstances = 0; + this.OnChange(); + } + +} diff --git a/src/dashboard/Synapse.Dashboard/Components/WorkflowDiagram/Templates/NodeLabelTemplate.razor b/src/dashboard/Synapse.Dashboard/Components/WorkflowDiagram/Templates/NodeLabelTemplate.razor new file mode 100644 index 000000000..52c5cb9ce --- /dev/null +++ b/src/dashboard/Synapse.Dashboard/Components/WorkflowDiagram/Templates/NodeLabelTemplate.razor @@ -0,0 +1,37 @@ +@namespace Synapse.Dashboard + +@if (!string.IsNullOrWhiteSpace(Node.Label)) +{ + + +
@Node.Label
+
+
+} + +@code { + [CascadingParameter(Name = "Node")] public INodeViewModel Node { get; set; } = null!; + + protected virtual string LabelX { get; set; } = "0"; + protected virtual string LabelY { get; set; } = "0"; + protected virtual string LabelWidth { get; set; } = ""; + protected virtual string LabelHeight { get; set; } = Neuroglia.Blazor.Dagre.Constants.LabelHeight.ToInvariantString(); + + protected override void OnParametersSet() + { + base.OnParametersSet(); + this.LabelX = (this.Node.BBox?.X ?? 0).ToInvariantString(); + this.LabelWidth = (this.Node.BBox?.Width ?? 0).ToInvariantString(); + if (this.Node is IClusterViewModel) + { + this.LabelY = ((this.Node.BBox?.Height ?? 0) / 2).ToInvariantString(); + } + else + { + this.LabelY = (0 - Neuroglia.Blazor.Dagre.Constants.LabelHeight / 2).ToInvariantString(); + } + } +} \ No newline at end of file diff --git a/src/dashboard/Synapse.Dashboard/Components/WorkflowDiagram/Templates/NodeShapeTemplate.razor b/src/dashboard/Synapse.Dashboard/Components/WorkflowDiagram/Templates/NodeShapeTemplate.razor new file mode 100644 index 000000000..d1676c7d3 --- /dev/null +++ b/src/dashboard/Synapse.Dashboard/Components/WorkflowDiagram/Templates/NodeShapeTemplate.razor @@ -0,0 +1,22 @@ +@namespace Synapse.Dashboard + + + @if (Node.Shape == NodeShape.Circle) + { + + } + else if (Node.Shape == NodeShape.Ellipse) + { + + } + else + { + + } + + +@code { + + [CascadingParameter(Name = "Node")] public INodeViewModel Node { get; set; } = null!; + +} \ No newline at end of file diff --git a/src/dashboard/Synapse.Dashboard/Components/WorkflowDiagram/Templates/TaskNodeTemplate.razor b/src/dashboard/Synapse.Dashboard/Components/WorkflowDiagram/Templates/TaskNodeTemplate.razor new file mode 100644 index 000000000..bf59c368c --- /dev/null +++ b/src/dashboard/Synapse.Dashboard/Components/WorkflowDiagram/Templates/TaskNodeTemplate.razor @@ -0,0 +1,23 @@ +@namespace Synapse.Dashboard +@using Neuroglia.Blazor.Dagre +@using Neuroglia.Blazor.Dagre.Templates +@inherits ClusterTemplate + + + + + + + + + + + @if (Graph.EnableProfiling) { + + } + + + +@code { + protected virtual TaskNodeViewModel TaskNode => (TaskNodeViewModel)this.Cluster; +} \ No newline at end of file diff --git a/src/dashboard/Synapse.Dashboard/Components/WorkflowDiagram/Templates/WorkflowNodeTemplate.razor b/src/dashboard/Synapse.Dashboard/Components/WorkflowDiagram/Templates/WorkflowNodeTemplate.razor new file mode 100644 index 000000000..0328f0a57 --- /dev/null +++ b/src/dashboard/Synapse.Dashboard/Components/WorkflowDiagram/Templates/WorkflowNodeTemplate.razor @@ -0,0 +1,24 @@ +@namespace Synapse.Dashboard +@inherits NodeTemplate + + + + + + + + + + + @if (Graph.EnableProfiling) { + + } + + + + +@code { + + protected virtual WorkflowNodeViewModel WorkflowNode => (WorkflowNodeViewModel)this.Node; + +} \ No newline at end of file diff --git a/src/dashboard/Synapse.Dashboard/Components/WorkflowDiagram/TryTaskNodeViewModel.cs b/src/dashboard/Synapse.Dashboard/Components/WorkflowDiagram/TryTaskNodeViewModel.cs new file mode 100644 index 000000000..be8b6f96c --- /dev/null +++ b/src/dashboard/Synapse.Dashboard/Components/WorkflowDiagram/TryTaskNodeViewModel.cs @@ -0,0 +1,35 @@ +// Copyright © 2024-Present The Synapse Authors +// +// Licensed under the Apache License, Version 2.0 (the "License"), +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +using Neuroglia.Blazor.Dagre; +using ServerlessWorkflow.Sdk.Models.Tasks; + +namespace Synapse.Dashboard.Components; + +/// +/// Represents a try task node view model +/// +public class TryTaskNodeViewModel + : LabeledWorkflowNodeViewModel +{ + + /// + /// Initializes a new + /// + public TryTaskNodeViewModel(KeyValuePair task) + : base(task.Key, "switch-task-node", null, Constants.NodeHeight * 1.5, Constants.NodeHeight * 1.5) + { + + } + +} diff --git a/src/dashboard/Synapse.Dashboard/Components/WorkflowDiagram/WaitTaskNodeViewModel.cs b/src/dashboard/Synapse.Dashboard/Components/WorkflowDiagram/WaitTaskNodeViewModel.cs new file mode 100644 index 000000000..03276669c --- /dev/null +++ b/src/dashboard/Synapse.Dashboard/Components/WorkflowDiagram/WaitTaskNodeViewModel.cs @@ -0,0 +1,35 @@ +// Copyright © 2024-Present The Synapse Authors +// +// Licensed under the Apache License, Version 2.0 (the "License"), +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +using Neuroglia.Blazor.Dagre; +using ServerlessWorkflow.Sdk.Models.Tasks; + +namespace Synapse.Dashboard.Components; + +/// +/// Represents a wait task node view model +/// +public class WaitTaskNodeViewModel + : LabeledWorkflowNodeViewModel +{ + + /// + /// Initializes a new + /// + public WaitTaskNodeViewModel(KeyValuePair task) + : base(task.Key, "wait-task-node", null, Constants.NodeHeight * 1.5, Constants.NodeHeight * 1.5) + { + + } + +} \ No newline at end of file diff --git a/src/dashboard/Synapse.Dashboard/Components/WorkflowDiagram/WorkflowDiagram.razor b/src/dashboard/Synapse.Dashboard/Components/WorkflowDiagram/WorkflowDiagram.razor new file mode 100644 index 000000000..f8bfeaaac --- /dev/null +++ b/src/dashboard/Synapse.Dashboard/Components/WorkflowDiagram/WorkflowDiagram.razor @@ -0,0 +1,59 @@ +@namespace Synapse.Dashboard +@using ServerlessWorkflow.Sdk.Models +@inject IWorkflowGraphBuilder WorkflowGraphBuilder + +@if (Graph != null) +{ + +} + +@code{ + + [Parameter] public WorkflowDefinition WorkflowDefinition { get; set; } = null!; + + protected WorkflowDefinition workflowDefinition { get; set; } = null!; + + [Parameter] public WorkflowDiagramOrientation Orientation { get; set; } = WorkflowDiagramOrientation.TopToBottom; + + [Parameter] public EventCallback> OnMouseUp { get; set; } + + public IGraphViewModel? Graph { get; set; } + protected DagreGraph? dagre { get; set; } + protected IDagreGraphOptions? options { get; set; } = null; + protected bool isDirty = true; + // Maps a state name to its cluster(s) in the graph + protected Dictionary>? TaskMap = null; + + protected override async Task OnParametersSetAsync() + { + await base.OnParametersSetAsync(); + if (this.WorkflowDefinition != null && this.WorkflowDefinition != this.workflowDefinition) + { + this.workflowDefinition = this.WorkflowDefinition; + this.options = new DagreGraphOptions() + { + Direction = this.Orientation == WorkflowDiagramOrientation.LeftToRight ? DagreGraphDirection.LeftToRight : DagreGraphDirection.TopToBottom + }; + var graph = await this.WorkflowGraphBuilder.Build(this.workflowDefinition); + this.Graph = graph; + this.TaskMap = this.Graph.AllClusters.Values.OfType() + .GroupBy(cluster => cluster.Task.Key) + .ToDictionary(group => group.Key, group => group.AsEnumerable()) + ; + this.isDirty = true; + } + } + + protected override bool ShouldRender() + { + if (!this.isDirty) return false; + this.isDirty = false; + return true; + } + + public virtual async Task RefreshAsync() + { + if (this.dagre != null) await this.dagre.RefreshAsync(); + } + +} \ No newline at end of file diff --git a/src/dashboard/Synapse.Dashboard/Components/WorkflowDiagram/WorkflowDiagramOrientation.cs b/src/dashboard/Synapse.Dashboard/Components/WorkflowDiagram/WorkflowDiagramOrientation.cs new file mode 100644 index 000000000..4c721d1ea --- /dev/null +++ b/src/dashboard/Synapse.Dashboard/Components/WorkflowDiagram/WorkflowDiagramOrientation.cs @@ -0,0 +1,29 @@ +// Copyright © 2024-Present The Synapse Authors +// +// Licensed under the Apache License, Version 2.0 (the "License"), +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +namespace Synapse.Dashboard.Components; + +/// +/// Enumerates all supported workflow diagram orientations +/// +public enum WorkflowDiagramOrientation +{ + /// + /// Indicates a top to bottom diagram + /// + TopToBottom, + /// + /// Indicates a left to right diagram + /// + LeftToRight +} \ No newline at end of file diff --git a/src/dashboard/Synapse.Dashboard/Pages/Authentication/Bearer/Login.razor b/src/dashboard/Synapse.Dashboard/Pages/Authentication/Bearer/Login.razor index cab4f82ca..c3df0337d 100644 --- a/src/dashboard/Synapse.Dashboard/Pages/Authentication/Bearer/Login.razor +++ b/src/dashboard/Synapse.Dashboard/Pages/Authentication/Bearer/Login.razor @@ -6,7 +6,7 @@ Token Authentication
- +
diff --git a/src/dashboard/Synapse.Dashboard/Pages/Workflows/List/View.razor b/src/dashboard/Synapse.Dashboard/Pages/Workflows/List/View.razor index 93da81ade..2a0f230ee 100644 --- a/src/dashboard/Synapse.Dashboard/Pages/Workflows/List/View.razor +++ b/src/dashboard/Synapse.Dashboard/Pages/Workflows/List/View.razor @@ -138,4 +138,14 @@ else throw new NotSupportedException("The specified schedule type is not supported"); } + new Task OnShowResourceDetailsAsync(Workflow workflow) + { + if (this.DetailsOffCanvas == null) return Task.CompletedTask; + var parameters = new Dictionary + { + { nameof(WorkflowDetails.Workflow), workflow } + }; + return this.DetailsOffCanvas.ShowAsync(title: workflow.GetName(), parameters: parameters); + } + } \ No newline at end of file diff --git a/src/dashboard/Synapse.Dashboard/Program.cs b/src/dashboard/Synapse.Dashboard/Program.cs index 04bf8e1b5..181421fd8 100644 --- a/src/dashboard/Synapse.Dashboard/Program.cs +++ b/src/dashboard/Synapse.Dashboard/Program.cs @@ -12,7 +12,7 @@ // limitations under the License. using Microsoft.AspNetCore.Components.Authorization; -using Microsoft.Extensions.DependencyInjection; +using Neuroglia.Blazor.Dagre; var builder = WebAssemblyHostBuilder.CreateDefault(args); @@ -41,6 +41,8 @@ builder.Services.AddSingleton(); builder.Services.AddSingleton(); builder.Services.AddSingleton(); +builder.Services.AddSingleton(); +builder.Services.AddSingleton(); builder.Services.AddBlazorBootstrap(); builder.Services.AddCascadingAuthenticationState(); builder.Services.AddAuthorizationCore(); diff --git a/src/dashboard/Synapse.Dashboard/Services/Interfaces/ISecurityTokenManager.cs b/src/dashboard/Synapse.Dashboard/Services/Interfaces/ISecurityTokenManager.cs index 3b0806792..bd4209029 100644 --- a/src/dashboard/Synapse.Dashboard/Services/Interfaces/ISecurityTokenManager.cs +++ b/src/dashboard/Synapse.Dashboard/Services/Interfaces/ISecurityTokenManager.cs @@ -34,4 +34,4 @@ public interface ISecurityTokenManager /// A new awaitable Task SetTokenAsync(string token, CancellationToken cancellationToken = default); -} \ No newline at end of file +} diff --git a/src/dashboard/Synapse.Dashboard/Services/Interfaces/IWorkflowGraphBuilder.cs b/src/dashboard/Synapse.Dashboard/Services/Interfaces/IWorkflowGraphBuilder.cs new file mode 100644 index 000000000..a9c26b9b8 --- /dev/null +++ b/src/dashboard/Synapse.Dashboard/Services/Interfaces/IWorkflowGraphBuilder.cs @@ -0,0 +1,32 @@ +// Copyright © 2024-Present The Synapse Authors +// +// Licensed under the Apache License, Version 2.0 (the "License"), +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +using Neuroglia.Blazor.Dagre.Models; +using ServerlessWorkflow.Sdk.Models; + +namespace Synapse.Dashboard.Services; + +/// +/// Defines the fundamentals of a service used to build workflow graphs +/// +public interface IWorkflowGraphBuilder +{ + + /// + /// Builds a new workflow + /// + /// The to build a new for + /// A new + Task Build(WorkflowDefinition workflow); + +} diff --git a/src/dashboard/Synapse.Dashboard/Services/LabeledWorkflowNodeViewModel.cs b/src/dashboard/Synapse.Dashboard/Services/LabeledWorkflowNodeViewModel.cs new file mode 100644 index 000000000..0a9f46982 --- /dev/null +++ b/src/dashboard/Synapse.Dashboard/Services/LabeledWorkflowNodeViewModel.cs @@ -0,0 +1,34 @@ +// Copyright © 2024-Present The Synapse Authors +// +// Licensed under the Apache License, Version 2.0 (the "License"), +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +using Neuroglia.Blazor.Dagre; +using Neuroglia.Blazor.Dagre.Models; + +namespace Synapse.Dashboard.Services; + +/// +/// Represents a containing a label +/// +public class LabeledWorkflowNodeViewModel + : WorkflowNodeViewModel +{ + + /// + public LabeledWorkflowNodeViewModel(string? label = "", string? cssClass = null, string? shape = null, double? width = Constants.NodeWidth * 2, double? height = Constants.NodeHeight, double ? radiusX = Constants.NodeRadius, double? radiusY = Constants.NodeRadius, double? x = 0, double? y = 0, Type? componentType = null, Guid? parentId = null) + : base(label, cssClass, shape, width, height, radiusX, radiusY, x, y, componentType, parentId) + { + + + } + +} \ No newline at end of file diff --git a/src/dashboard/Synapse.Dashboard/Services/WorkflowGraphBuilder.cs b/src/dashboard/Synapse.Dashboard/Services/WorkflowGraphBuilder.cs new file mode 100644 index 000000000..3060b9a13 --- /dev/null +++ b/src/dashboard/Synapse.Dashboard/Services/WorkflowGraphBuilder.cs @@ -0,0 +1,443 @@ +// Copyright © 2024-Present The Synapse Authors +// +// Licensed under the Apache License, Version 2.0 (the "License"), +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +using Neuroglia.Blazor.Dagre.Models; +using ServerlessWorkflow.Sdk; +using ServerlessWorkflow.Sdk.Models; +using ServerlessWorkflow.Sdk.Models.Tasks; +using Synapse.Dashboard.StateManagement; +using static Synapse.SynapseDefaults.Resources; + +namespace Synapse.Dashboard.Services; + +/// +/// Represents the default implementation of the interface +/// +public class WorkflowGraphBuilder + : IWorkflowGraphBuilder +{ + + /// + /// Gets the default radius for start and end nodes + /// + public const int StartEndNodeRadius = 30; + public const double GraphBagdesRadius = 15; + public const double GraphStartEndNodeRadius = 30; + public const string SpecVersion = "0.8"; + public const string DefinitionVersion = "1.0"; + + /// + public async Task Build(WorkflowDefinition workflow) + { + ArgumentNullException.ThrowIfNull(workflow); + var isEmpty = workflow.Do.Count < 1; + var graph = new GraphViewModel(/*enableProfiling: true*/); + //graph.RegisterBehavior(new DragAndDropNodeBehavior(graph, this.jSRuntime)); + var startNode = this.BuildStartNode(!isEmpty); + var endNode = this.BuildEndNode(); + await graph.AddElementAsync(startNode); + if (isEmpty) await this.BuildEdgeAsync(graph, startNode, endNode); + else await this.BuildTaskNodeAsync(workflow, graph, workflow.Do.First(), endNode, startNode, "/do", true); + await graph.AddElementAsync(endNode); + return graph; + } + + /// + /// Builds a new start + /// + /// A boolean indicating whether or not the node has successor + /// A new + protected virtual NodeViewModel BuildStartNode(bool hasSuccessor = false) => new StartNodeViewModel(hasSuccessor); + + /// + /// Builds a new for the specified task + /// + /// The that defines the to create a new for + /// The current + /// The name/definition mapping of the to create a new for + /// The end node + /// The previous node + /// A reference to the parent, if any, of the task to build a new for + /// Indicates whether or not the task to create the for is the first task of the workflow it belongs to + /// A new + protected async Task BuildTaskNodeAsync(WorkflowDefinition workflow, GraphViewModel graph, KeyValuePair task, NodeViewModel endNode, NodeViewModel previousNode, string parentReference, bool first = false) + { + ArgumentNullException.ThrowIfNull(workflow); + ArgumentNullException.ThrowIfNull(graph); + ArgumentNullException.ThrowIfNull(task); + ArgumentNullException.ThrowIfNull(endNode); + ArgumentNullException.ThrowIfNull(previousNode); + var taskNodeGroup = graph.AllClusters.Values.OfType().FirstOrDefault(cluster => cluster.Task.Key == task.Key); + if (taskNodeGroup != null) return taskNodeGroup; + var lastNode = task.Value switch + { + CallTaskDefinition callTask => await this.BuildCallTaskNodeAsync(workflow, graph, new(task.Key, callTask), taskNodeGroup, endNode, previousNode, parentReference), + CompositeTaskDefinition compositeTask => await this.BuildCompositeTaskNodeAsync(workflow, graph, new(task.Key, compositeTask), taskNodeGroup, endNode, previousNode, parentReference), + EmitTaskDefinition emitTask => await this.BuildEmitTaskNodeAsync(workflow, graph, new(task.Key, emitTask), taskNodeGroup, endNode, previousNode, parentReference), + ExtensionTaskDefinition extensionTask => await this.BuildExtensionTaskNodeAsync(workflow, graph, new(task.Key, extensionTask), taskNodeGroup, endNode, previousNode, parentReference), + ForTaskDefinition forTask => await this.BuildForTaskNodeAsync(workflow, graph, new(task.Key, forTask), taskNodeGroup, endNode, previousNode, parentReference), + ListenTaskDefinition listenTask => await this.BuildListenTaskNodeAsync(workflow, graph, new(task.Key, listenTask), taskNodeGroup, endNode, previousNode, parentReference), + RaiseTaskDefinition raiseTask => await this.BuildRaiseTaskNodeAsync(workflow, graph, new(task.Key, raiseTask), taskNodeGroup, endNode, previousNode, parentReference), + RunTaskDefinition runTask => await this.BuildRunTaskNodeAsync(workflow, graph, new(task.Key, runTask), taskNodeGroup, endNode, previousNode, parentReference), + SetTaskDefinition setTask => await this.BuildSetTaskNodeAsync(workflow, graph, new(task.Key, setTask), taskNodeGroup, endNode, previousNode, parentReference), + SwitchTaskDefinition switchTask => await this.BuildSwitchTaskNodeAsync(workflow, graph, new(task.Key, switchTask), taskNodeGroup, endNode, previousNode, parentReference), + TryTaskDefinition tryTask => await this.BuildTryTaskNodeAsync(workflow, graph, new(task.Key, tryTask), taskNodeGroup, endNode, previousNode, parentReference), + WaitTaskDefinition waitTask => await this.BuildWaitTaskNodeAsync(workflow, graph, new(task.Key, waitTask), taskNodeGroup, endNode, previousNode, parentReference), + _ => throw new NotSupportedException($"The specified task type '{task.Value.GetType()}' is not supported") + } ?? throw new Exception($"Unable to define a last node for task '{task.Key}'"); + if (task.Value.Then == FlowDirective.End || task.Value.Then == FlowDirective.Exit) await this.BuildEdgeAsync(graph, lastNode, endNode); + else + { + var nextTaskName = string.IsNullOrWhiteSpace(task.Value.Then) || task.Value.Then == FlowDirective.Continue + ? workflow.GetNextTask(task, parentReference)?.Key + : task.Value.Then; + if(string.IsNullOrWhiteSpace(nextTaskName)) await this.BuildEdgeAsync(graph, lastNode, endNode); + else + { + var nextTaskReference = $"{parentReference}/{nextTaskName}"; + var nextTask = workflow.GetComponent(nextTaskReference) ?? throw new Exception($"Failed to find the task at '{nextTaskReference}' in workflow '{workflow.Document.Name}.{workflow.Document.Namespace}:{workflow.Document.Version}'"); + var nextTaskNode = await this.BuildTaskNodeAsync(workflow, graph, new(nextTaskName, nextTask), endNode, lastNode, parentReference); + //await this.BuildEdgeAsync(graph, lastNode, nextTaskNode.Children.Values.OfType().First()); //todo + } + } + return lastNode; + } + + /// + /// Builds a new for the specified task + /// + /// The that defines the to create a new for + /// The current + /// The name/definition mapping of the to create a new for + /// The current task node group + /// The end node + /// The previous node + /// The reference of the task's parent + /// A new + protected virtual async Task BuildCallTaskNodeAsync(WorkflowDefinition workflow, GraphViewModel graph, KeyValuePair task, TaskNodeViewModel? taskNodeGroup, NodeViewModel endNode, NodeViewModel previousNode, string parentReference) + { + ArgumentNullException.ThrowIfNull(workflow); + ArgumentNullException.ThrowIfNull(graph); + ArgumentNullException.ThrowIfNull(task); + ArgumentNullException.ThrowIfNull(endNode); + ArgumentNullException.ThrowIfNull(previousNode); + ArgumentException.ThrowIfNullOrWhiteSpace(parentReference); + var lastNode = new CallTaskNodeViewModel(task); + if (taskNodeGroup == null) await graph.AddElementAsync(lastNode); + else await taskNodeGroup.AddChildAsync(lastNode); + await this.BuildEdgeAsync(graph, previousNode, lastNode); + return lastNode; + } + + /// + /// Builds a new for the specified task + /// + /// The that defines the to create a new for + /// The current + /// The name/definition mapping of the to create a new for + /// The current task node group + /// The end node + /// The previous node + /// The reference of the task's parent + /// A new + protected virtual async Task BuildCompositeTaskNodeAsync(WorkflowDefinition workflow, GraphViewModel graph, KeyValuePair task, TaskNodeViewModel? taskNodeGroup, NodeViewModel endNode, NodeViewModel previousNode, string parentReference) + { + ArgumentNullException.ThrowIfNull(workflow); + ArgumentNullException.ThrowIfNull(graph); + ArgumentNullException.ThrowIfNull(task); + ArgumentNullException.ThrowIfNull(endNode); + ArgumentNullException.ThrowIfNull(previousNode); + ArgumentException.ThrowIfNullOrWhiteSpace(parentReference); + var lastNode = new CompositeTaskNodeViewModel(task); + if (taskNodeGroup == null) await graph.AddElementAsync(lastNode); + else await taskNodeGroup.AddChildAsync(lastNode); + await this.BuildEdgeAsync(graph, previousNode, lastNode); + return lastNode; + } + + /// + /// Builds a new for the specified task + /// + /// The that defines the to create a new for + /// The current + /// The name/definition mapping of the to create a new for + /// The current task node group + /// The end node + /// The previous node + /// The reference of the task's parent + /// A new + protected virtual async Task BuildEmitTaskNodeAsync(WorkflowDefinition workflow, GraphViewModel graph, KeyValuePair task, TaskNodeViewModel? taskNodeGroup, NodeViewModel endNode, NodeViewModel previousNode, string parentReference) + { + ArgumentNullException.ThrowIfNull(workflow); + ArgumentNullException.ThrowIfNull(graph); + ArgumentNullException.ThrowIfNull(task); + ArgumentNullException.ThrowIfNull(endNode); + ArgumentNullException.ThrowIfNull(previousNode); + ArgumentException.ThrowIfNullOrWhiteSpace(parentReference); + var lastNode = new EmitTaskNodeViewModel(task); + if (taskNodeGroup == null) await graph.AddElementAsync(lastNode); + else await taskNodeGroup.AddChildAsync(lastNode); + await this.BuildEdgeAsync(graph, previousNode, lastNode); + return lastNode; + } + + /// + /// Builds a new for the specified task + /// + /// The that defines the to create a new for + /// The current + /// The name/definition mapping of the to create a new for + /// The current task node group + /// The end node + /// The previous node + /// The reference of the task's parent + /// A new + protected virtual async Task BuildExtensionTaskNodeAsync(WorkflowDefinition workflow, GraphViewModel graph, KeyValuePair task, TaskNodeViewModel? taskNodeGroup, NodeViewModel endNode, NodeViewModel previousNode, string parentReference) + { + ArgumentNullException.ThrowIfNull(workflow); + ArgumentNullException.ThrowIfNull(graph); + ArgumentNullException.ThrowIfNull(task); + ArgumentNullException.ThrowIfNull(endNode); + ArgumentNullException.ThrowIfNull(previousNode); + ArgumentException.ThrowIfNullOrWhiteSpace(parentReference); + var lastNode = new ExtensionTaskNodeViewModel(task); + if (taskNodeGroup == null) await graph.AddElementAsync(lastNode); + else await taskNodeGroup.AddChildAsync(lastNode); + await this.BuildEdgeAsync(graph, previousNode, lastNode); + return lastNode; + } + + /// + /// Builds a new for the specified task + /// + /// The that defines the to create a new for + /// The current + /// The name/definition mapping of the to create a new for + /// The current task node group + /// The end node + /// The previous node + /// The reference of the task's parent + /// A new + protected virtual async Task BuildForTaskNodeAsync(WorkflowDefinition workflow, GraphViewModel graph, KeyValuePair task, TaskNodeViewModel? taskNodeGroup, NodeViewModel endNode, NodeViewModel previousNode, string parentReference) + { + ArgumentNullException.ThrowIfNull(workflow); + ArgumentNullException.ThrowIfNull(graph); + ArgumentNullException.ThrowIfNull(task); + ArgumentNullException.ThrowIfNull(endNode); + ArgumentNullException.ThrowIfNull(previousNode); + ArgumentException.ThrowIfNullOrWhiteSpace(parentReference); + var lastNode = new ForTaskNodeViewModel(task); + if (taskNodeGroup == null) await graph.AddElementAsync(lastNode); + else await taskNodeGroup.AddChildAsync(lastNode); + await this.BuildEdgeAsync(graph, previousNode, lastNode); + return lastNode; + } + + /// + /// Builds a new for the specified task + /// + /// The that defines the to create a new for + /// The current + /// The name/definition mapping of the to create a new for + /// The current task node group + /// The end node + /// The previous node + /// The reference of the task's parent + /// A new + protected virtual async Task BuildListenTaskNodeAsync(WorkflowDefinition workflow, GraphViewModel graph, KeyValuePair task, TaskNodeViewModel? taskNodeGroup, NodeViewModel endNode, NodeViewModel previousNode, string parentReference) + { + ArgumentNullException.ThrowIfNull(workflow); + ArgumentNullException.ThrowIfNull(graph); + ArgumentNullException.ThrowIfNull(task); + ArgumentNullException.ThrowIfNull(endNode); + ArgumentNullException.ThrowIfNull(previousNode); + ArgumentException.ThrowIfNullOrWhiteSpace(parentReference); + var lastNode = new ListenTaskNodeViewModel(task); + if (taskNodeGroup == null) await graph.AddElementAsync(lastNode); + else await taskNodeGroup.AddChildAsync(lastNode); + await this.BuildEdgeAsync(graph, previousNode, lastNode); + return lastNode; + } + + /// + /// Builds a new for the specified task + /// + /// The that defines the to create a new for + /// The current + /// The name/definition mapping of the to create a new for + /// The current task node group + /// The end node + /// The previous node + /// The reference of the task's parent + /// A new + protected virtual async Task BuildRaiseTaskNodeAsync(WorkflowDefinition workflow, GraphViewModel graph, KeyValuePair task, TaskNodeViewModel? taskNodeGroup, NodeViewModel endNode, NodeViewModel previousNode, string parentReference) + { + ArgumentNullException.ThrowIfNull(workflow); + ArgumentNullException.ThrowIfNull(graph); + ArgumentNullException.ThrowIfNull(task); + ArgumentNullException.ThrowIfNull(endNode); + ArgumentNullException.ThrowIfNull(previousNode); + ArgumentException.ThrowIfNullOrWhiteSpace(parentReference); + var lastNode = new RaiseTaskNodeViewModel(task); + if (taskNodeGroup == null) await graph.AddElementAsync(lastNode); + else await taskNodeGroup.AddChildAsync(lastNode); + await this.BuildEdgeAsync(graph, previousNode, lastNode); + return lastNode; + } + + /// + /// Builds a new for the specified task + /// + /// The that defines the to create a new for + /// The current + /// The name/definition mapping of the to create a new for + /// The current task node group + /// The end node + /// The previous node + /// The reference of the task's parent + /// A new + protected virtual async Task BuildRunTaskNodeAsync(WorkflowDefinition workflow, GraphViewModel graph, KeyValuePair task, TaskNodeViewModel? taskNodeGroup, NodeViewModel endNode, NodeViewModel previousNode, string parentReference) + { + ArgumentNullException.ThrowIfNull(workflow); + ArgumentNullException.ThrowIfNull(graph); + ArgumentNullException.ThrowIfNull(task); + ArgumentNullException.ThrowIfNull(endNode); + ArgumentNullException.ThrowIfNull(previousNode); + ArgumentException.ThrowIfNullOrWhiteSpace(parentReference); + var lastNode = new RunTaskNodeViewModel(task); + if (taskNodeGroup == null) await graph.AddElementAsync(lastNode); + else await taskNodeGroup.AddChildAsync(lastNode); + await this.BuildEdgeAsync(graph, previousNode, lastNode); + return lastNode; + } + + /// + /// Builds a new for the specified task + /// + /// The that defines the to create a new for + /// The current + /// The name/definition mapping of the to create a new for + /// The current task node group + /// The end node + /// The previous node + /// The reference of the task's parent + /// A new + protected virtual async Task BuildSetTaskNodeAsync(WorkflowDefinition workflow, GraphViewModel graph, KeyValuePair task, TaskNodeViewModel? taskNodeGroup, NodeViewModel endNode, NodeViewModel previousNode, string parentReference) + { + ArgumentNullException.ThrowIfNull(workflow); + ArgumentNullException.ThrowIfNull(graph); + ArgumentNullException.ThrowIfNull(task); + ArgumentNullException.ThrowIfNull(endNode); + ArgumentNullException.ThrowIfNull(previousNode); + ArgumentException.ThrowIfNullOrWhiteSpace(parentReference); + var lastNode = new SetTaskNodeViewModel(task); + if (taskNodeGroup == null) await graph.AddElementAsync(lastNode); + else await taskNodeGroup.AddChildAsync(lastNode); + await this.BuildEdgeAsync(graph, previousNode, lastNode); + return lastNode; + } + + /// + /// Builds a new for the specified task + /// + /// The that defines the to create a new for + /// The current + /// The name/definition mapping of the to create a new for + /// The current task node group + /// The end node + /// The previous node + /// The reference of the task's parent + /// A new + protected virtual async Task BuildSwitchTaskNodeAsync(WorkflowDefinition workflow, GraphViewModel graph, KeyValuePair task, TaskNodeViewModel? taskNodeGroup, NodeViewModel endNode, NodeViewModel previousNode, string parentReference) + { + ArgumentNullException.ThrowIfNull(workflow); + ArgumentNullException.ThrowIfNull(graph); + ArgumentNullException.ThrowIfNull(task); + ArgumentNullException.ThrowIfNull(endNode); + ArgumentNullException.ThrowIfNull(previousNode); + ArgumentException.ThrowIfNullOrWhiteSpace(parentReference); + var lastNode = new SwitchTaskNodeViewModel(task); + if (taskNodeGroup == null) await graph.AddElementAsync(lastNode); + else await taskNodeGroup.AddChildAsync(lastNode); + await this.BuildEdgeAsync(graph, previousNode, lastNode); + return lastNode; + } + + /// + /// Builds a new for the specified task + /// + /// The that defines the to create a new for + /// The current + /// The name/definition mapping of the to create a new for + /// The current task node group + /// The end node + /// The previous node + /// The reference of the task's parent + /// A new + protected virtual async Task BuildTryTaskNodeAsync(WorkflowDefinition workflow, GraphViewModel graph, KeyValuePair task, TaskNodeViewModel? taskNodeGroup, NodeViewModel endNode, NodeViewModel previousNode, string parentReference) + { + ArgumentNullException.ThrowIfNull(workflow); + ArgumentNullException.ThrowIfNull(graph); + ArgumentNullException.ThrowIfNull(task); + ArgumentNullException.ThrowIfNull(endNode); + ArgumentNullException.ThrowIfNull(previousNode); + ArgumentException.ThrowIfNullOrWhiteSpace(parentReference); + var lastNode = new TryTaskNodeViewModel(task); + if (taskNodeGroup == null) await graph.AddElementAsync(lastNode); + else await taskNodeGroup.AddChildAsync(lastNode); + await this.BuildEdgeAsync(graph, previousNode, lastNode); + return lastNode; + } + + /// + /// Builds a new for the specified task + /// + /// The that defines the to create a new for + /// The current + /// The name/definition mapping of the to create a new for + /// The current task node group + /// The end node + /// The previous node + /// The reference of the task's parent + /// A new + protected virtual async Task BuildWaitTaskNodeAsync(WorkflowDefinition workflow, GraphViewModel graph, KeyValuePair task, TaskNodeViewModel? taskNodeGroup, NodeViewModel endNode, NodeViewModel previousNode, string parentReference) + { + ArgumentNullException.ThrowIfNull(workflow); + ArgumentNullException.ThrowIfNull(graph); + ArgumentNullException.ThrowIfNull(task); + ArgumentNullException.ThrowIfNull(endNode); + ArgumentNullException.ThrowIfNull(previousNode); + ArgumentException.ThrowIfNullOrWhiteSpace(parentReference); + var lastNode = new WaitTaskNodeViewModel(task); + if (taskNodeGroup == null) await graph.AddElementAsync(lastNode); + else await taskNodeGroup.AddChildAsync(lastNode); + await this.BuildEdgeAsync(graph, previousNode, lastNode); + return lastNode; + } + + /// + /// Builds a new end + /// + /// A new + protected virtual NodeViewModel BuildEndNode() => new EndNodeViewModel(); + + /// + /// Builds an edge between two nodes + /// + /// The current + /// The node to draw the edge from + /// The node to draw the edge to + /// A new awaitable + protected virtual Task BuildEdgeAsync(GraphViewModel graph, NodeViewModel source, NodeViewModel target) => graph.AddElementAsync(new EdgeViewModel(source.Id, target.Id, null)); + +} diff --git a/src/dashboard/Synapse.Dashboard/Services/WorkflowNodeViewModel.cs b/src/dashboard/Synapse.Dashboard/Services/WorkflowNodeViewModel.cs new file mode 100644 index 000000000..b438a8928 --- /dev/null +++ b/src/dashboard/Synapse.Dashboard/Services/WorkflowNodeViewModel.cs @@ -0,0 +1,67 @@ +// Copyright © 2024-Present The Synapse Authors +// +// Licensed under the Apache License, Version 2.0 (the "License"), +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +using Neuroglia.Blazor.Dagre; +using Neuroglia.Blazor.Dagre.Models; + +namespace Synapse.Dashboard.Services; + +/// +/// Represents the base class for all workflow-related node models +/// +public abstract class WorkflowNodeViewModel + : NodeViewModel, IWorkflowNodeViewModel +{ + + int _operativeInstances = 0; + int _faultedInstances = 0; + + /// + public WorkflowNodeViewModel(string? label = "", string? cssClass = null, string? shape = null, double? width = Constants.NodeWidth * 1.5, double? height = Constants.NodeHeight * 1.5, + double? radiusX = Constants.NodeRadius, double? radiusY = Constants.NodeRadius, double? x = 0, double? y = 0, Type? componentType = null, Guid? parentId = null) + : base(label, cssClass, shape, width, height, radiusX, radiusY, x, y, componentType, parentId) + { + this.ComponentType = typeof(WorkflowNodeTemplate); + } + + /// + public int OperativeInstancesCount + { + get => this._operativeInstances; + set + { + this._operativeInstances = value; + this.OnChange(); + } + } + + /// + public int FaultedInstancesCount + { + get => this._faultedInstances; + set + { + this._faultedInstances = value; + this.OnChange(); + } + } + + /// + public void ResetInstancesCount() + { + this._operativeInstances = 0; + this._faultedInstances = 0; + this.OnChange(); + } + +} diff --git a/src/dashboard/Synapse.Dashboard/Synapse.Dashboard.csproj b/src/dashboard/Synapse.Dashboard/Synapse.Dashboard.csproj index 498ee9ac3..3e3eb2d7a 100644 --- a/src/dashboard/Synapse.Dashboard/Synapse.Dashboard.csproj +++ b/src/dashboard/Synapse.Dashboard/Synapse.Dashboard.csproj @@ -16,6 +16,7 @@ + diff --git a/src/dashboard/Synapse.Dashboard/_Imports.razor b/src/dashboard/Synapse.Dashboard/_Imports.razor index 72aaa3128..a32b6ecdf 100644 --- a/src/dashboard/Synapse.Dashboard/_Imports.razor +++ b/src/dashboard/Synapse.Dashboard/_Imports.razor @@ -12,6 +12,9 @@ @using Microsoft.AspNetCore.Components.WebAssembly.Http @using Microsoft.Extensions.Options @using Microsoft.JSInterop +@using Neuroglia.Blazor.Dagre +@using Neuroglia.Blazor.Dagre.Models +@using Neuroglia.Blazor.Dagre.Templates @using Synapse.Dashboard @using Synapse.Dashboard.Components @using Synapse.Dashboard.Configuration diff --git a/src/dashboard/Synapse.Dashboard/wwwroot/index.html b/src/dashboard/Synapse.Dashboard/wwwroot/index.html index bc85c746f..896a22f28 100644 --- a/src/dashboard/Synapse.Dashboard/wwwroot/index.html +++ b/src/dashboard/Synapse.Dashboard/wwwroot/index.html @@ -40,6 +40,7 @@ + @@ -59,6 +60,9 @@ + + + @@ -87,6 +91,8 @@ + + diff --git a/src/runner/Synapse.Runner/Services/WorkflowExecutionContext.cs b/src/runner/Synapse.Runner/Services/WorkflowExecutionContext.cs index aa30d9793..a08b3cdb1 100644 --- a/src/runner/Synapse.Runner/Services/WorkflowExecutionContext.cs +++ b/src/runner/Synapse.Runner/Services/WorkflowExecutionContext.cs @@ -217,65 +217,57 @@ public virtual async Task ResumeAsync(CancellationToken cancellationToken = defa public virtual async Task CorrelateAsync(ITaskExecutionContext task, CancellationToken cancellationToken = default) { ArgumentNullException.ThrowIfNull(task); - try + if (task.Definition is not ListenTaskDefinition listenTask) throw new ArgumentException("The specified task's definition must be a 'listen' task", nameof(task)); + if (this.Instance.Status?.Correlation?.Contexts?.TryGetValue(task.Instance.Reference.OriginalString, out var context) == true && context != null) return context; + var @namespace = task.Workflow.Instance.GetNamespace()!; + var name = $"{@namespace}.{task.Workflow.Instance.GetName()}.{task.Instance.Id}"; + Correlation? correlation = null; + try { correlation = await this.Api.Correlations.GetAsync(name, @namespace, cancellationToken).ConfigureAwait(false); } + catch { } + if (correlation == null) { - if (task.Definition is not ListenTaskDefinition listenTask) throw new ArgumentException("The specified task's definition must be a 'listen' task", nameof(task)); - if (this.Instance.Status?.Correlation?.Contexts?.TryGetValue(task.Instance.Reference.OriginalString, out var context) == true && context != null) return context; - var @namespace = task.Workflow.Instance.GetNamespace()!; - var name = $"{@namespace}.{task.Workflow.Instance.GetName()}.{task.Instance.Id}"; - Correlation? correlation = null; - try { correlation = await this.Api.Correlations.GetAsync(name, @namespace, cancellationToken).ConfigureAwait(false); } - catch { } - if (correlation == null) + correlation = await this.Api.Correlations.CreateAsync(new() { - correlation = await this.Api.Correlations.CreateAsync(new() + Metadata = new() { - Metadata = new() - { - Namespace = @namespace, - Name = name - }, - Spec = new() + Namespace = @namespace, + Name = name + }, + Spec = new() + { + Source = new ResourceReference(task.Workflow.Instance.GetName(), task.Workflow.Instance.GetNamespace()), + Lifetime = CorrelationLifetime.Ephemeral, + Events = listenTask.Listen.To, + Expressions = task.Workflow.Definition.Evaluate ?? new(), + Outcome = new() { - Source = new ResourceReference(task.Workflow.Instance.GetName(), task.Workflow.Instance.GetNamespace()), - Lifetime = CorrelationLifetime.Ephemeral, - Events = listenTask.Listen.To, - Expressions = task.Workflow.Definition.Evaluate ?? new(), - Outcome = new() + Correlate = new() { - Correlate = new() - { - Instance = task.Workflow.Instance.GetQualifiedName(), - Task = task.Instance.Reference.OriginalString - } + Instance = task.Workflow.Instance.GetQualifiedName(), + Task = task.Instance.Reference.OriginalString } } - }, cancellationToken).ConfigureAwait(false); - } - var taskCompletionSource = new TaskCompletionSource(); - using var cancellationTokenRegistration = cancellationToken.Register(() => taskCompletionSource.TrySetCanceled()); - using var subscription = (await this.Api.WorkflowInstances.MonitorAsync(this.Instance.GetName(), this.Instance.GetNamespace()!, cancellationToken)) - .ToObservable() - .Where(e => e.Type == ResourceWatchEventType.Updated) - .Select(e => e.Resource.Status?.Correlation?.Contexts) - .Scan((Previous: (EquatableDictionary?)null, Current: (EquatableDictionary?)null), (accumulator, current) => (accumulator.Current ?? [], current)) - .Where(v => v.Current?.Count > v.Previous?.Count) //ensures we are not handling changes in a circular loop: if length of current is smaller than previous, it means a context has been processed - .Subscribe(value => - { - var patch = JsonPatchUtility.CreateJsonPatchFromDiff(value.Previous, value.Current); - var patchOperation = patch.Operations.FirstOrDefault(o => o.Op == OperationType.Add && o.Path.Segments.First().Value == task.Instance.Reference.OriginalString); - if (patchOperation == null) return; - context = this.JsonSerializer.Deserialize(patchOperation.Value!)!; - taskCompletionSource.SetResult(context); - }); - //todo: after a given amount of time, stop the execution of the workflow instance and put it to sleep - return await taskCompletionSource.Task.ConfigureAwait(false); - } - catch(Exception ex) - { - throw; + } + }, cancellationToken).ConfigureAwait(false); } - + var taskCompletionSource = new TaskCompletionSource(); + using var cancellationTokenRegistration = cancellationToken.Register(() => taskCompletionSource.TrySetCanceled()); + using var subscription = (await this.Api.WorkflowInstances.MonitorAsync(this.Instance.GetName(), this.Instance.GetNamespace()!, cancellationToken)) + .ToObservable() + .Where(e => e.Type == ResourceWatchEventType.Updated) + .Select(e => e.Resource.Status?.Correlation?.Contexts) + .Scan((Previous: (EquatableDictionary?)null, Current: (EquatableDictionary?)null), (accumulator, current) => (accumulator.Current ?? [], current)) + .Where(v => v.Current?.Count > v.Previous?.Count) //ensures we are not handling changes in a circular loop: if length of current is smaller than previous, it means a context has been processed + .Subscribe(value => + { + var patch = JsonPatchUtility.CreateJsonPatchFromDiff(value.Previous, value.Current); + var patchOperation = patch.Operations.FirstOrDefault(o => o.Op == OperationType.Add && o.Path.First() == task.Instance.Reference.OriginalString); + if (patchOperation == null) return; + context = this.JsonSerializer.Deserialize(patchOperation.Value!)!; + taskCompletionSource.SetResult(context); + }); + //todo: after a given amount of time, stop the execution of the workflow instance and put it to sleep + return await taskCompletionSource.Task.ConfigureAwait(false); } /// diff --git a/src/runner/Synapse.Runner/Synapse.Runner.csproj b/src/runner/Synapse.Runner/Synapse.Runner.csproj index 7cc8394dd..49aa9c119 100644 --- a/src/runner/Synapse.Runner/Synapse.Runner.csproj +++ b/src/runner/Synapse.Runner/Synapse.Runner.csproj @@ -40,11 +40,11 @@ - - - - - + + + + + diff --git a/tests/Synapse.IntegrationTests/Synapse.IntegrationTests.csproj b/tests/Synapse.IntegrationTests/Synapse.IntegrationTests.csproj index ecb3203bd..ec1d6221f 100644 --- a/tests/Synapse.IntegrationTests/Synapse.IntegrationTests.csproj +++ b/tests/Synapse.IntegrationTests/Synapse.IntegrationTests.csproj @@ -18,7 +18,7 @@ - + all diff --git a/tests/Synapse.UnitTests/Synapse.UnitTests.csproj b/tests/Synapse.UnitTests/Synapse.UnitTests.csproj index 52d4fd8e5..9e59d8c2d 100644 --- a/tests/Synapse.UnitTests/Synapse.UnitTests.csproj +++ b/tests/Synapse.UnitTests/Synapse.UnitTests.csproj @@ -19,12 +19,12 @@ - - - + + + - +