diff --git a/src/api/Synapse.Api.Http/Synapse.Api.Http.csproj b/src/api/Synapse.Api.Http/Synapse.Api.Http.csproj
index 79dde15b..56f69663 100644
--- a/src/api/Synapse.Api.Http/Synapse.Api.Http.csproj
+++ b/src/api/Synapse.Api.Http/Synapse.Api.Http.csproj
@@ -43,8 +43,8 @@
-
-
+
+
diff --git a/src/core/Synapse.Core.Infrastructure/Synapse.Core.Infrastructure.csproj b/src/core/Synapse.Core.Infrastructure/Synapse.Core.Infrastructure.csproj
index b8ee60af..f4a3c603 100644
--- a/src/core/Synapse.Core.Infrastructure/Synapse.Core.Infrastructure.csproj
+++ b/src/core/Synapse.Core.Infrastructure/Synapse.Core.Infrastructure.csproj
@@ -45,12 +45,12 @@
-
-
-
-
-
-
+
+
+
+
+
+
diff --git a/src/core/Synapse.Core/Synapse.Core.csproj b/src/core/Synapse.Core/Synapse.Core.csproj
index 49735fac..b652a32b 100644
--- a/src/core/Synapse.Core/Synapse.Core.csproj
+++ b/src/core/Synapse.Core/Synapse.Core.csproj
@@ -67,8 +67,8 @@
-
-
+
+
diff --git a/src/correlator/Synapse.Correlator/Synapse.Correlator.csproj b/src/correlator/Synapse.Correlator/Synapse.Correlator.csproj
index 5fd9a37a..e0f77911 100644
--- a/src/correlator/Synapse.Correlator/Synapse.Correlator.csproj
+++ b/src/correlator/Synapse.Correlator/Synapse.Correlator.csproj
@@ -36,14 +36,14 @@
-
-
-
-
-
-
+
+
+
+
+
+
-
+
diff --git a/src/dashboard/Synapse.Dashboard/Components/WorkflowDiagram/StartNodeViewModel.cs b/src/dashboard/Synapse.Dashboard/Components/WorkflowDiagram/StartNodeViewModel.cs
index e04dd9a0..2c997f1d 100644
--- a/src/dashboard/Synapse.Dashboard/Components/WorkflowDiagram/StartNodeViewModel.cs
+++ b/src/dashboard/Synapse.Dashboard/Components/WorkflowDiagram/StartNodeViewModel.cs
@@ -18,13 +18,7 @@ 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)
+public class StartNodeViewModel()
: WorkflowNodeViewModel("start-node", new() { CssClass = "start-node", Shape = NodeShape.Circle, Width = WorkflowGraphBuilder.StartEndNodeRadius, Height = 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/Pages/Workflows/Create/State.cs b/src/dashboard/Synapse.Dashboard/Pages/Workflows/Create/State.cs
index 38aeae13..96c06c65 100644
--- a/src/dashboard/Synapse.Dashboard/Pages/Workflows/Create/State.cs
+++ b/src/dashboard/Synapse.Dashboard/Pages/Workflows/Create/State.cs
@@ -12,7 +12,6 @@
// limitations under the License.
using ServerlessWorkflow.Sdk.Models;
-using Synapse.Resources;
namespace Synapse.Dashboard.Pages.Workflows.Create;
diff --git a/src/dashboard/Synapse.Dashboard/Services/WorkflowGraphBuilder.cs b/src/dashboard/Synapse.Dashboard/Services/WorkflowGraphBuilder.cs
index 8fd579c6..8f332508 100644
--- a/src/dashboard/Synapse.Dashboard/Services/WorkflowGraphBuilder.cs
+++ b/src/dashboard/Synapse.Dashboard/Services/WorkflowGraphBuilder.cs
@@ -17,7 +17,6 @@
using ServerlessWorkflow.Sdk.Models;
using ServerlessWorkflow.Sdk.Models.Calls;
using ServerlessWorkflow.Sdk.Models.Tasks;
-using Synapse.Dashboard.Components.DocumentDetailsStateManagement;
using System.Diagnostics;
namespace Synapse.Dashboard.Services;
@@ -32,7 +31,8 @@ public class WorkflowGraphBuilder(ILogger logger, IYamlSer
: IWorkflowGraphBuilder
{
- const string _portSuffix = "-port";
+ const string _clusterEntrySuffix = "-cluster-entry-port";
+ const string _clusterExitSuffix = "-cluster-exit-port";
const string _trySuffix = "-try";
const string _catchSuffix = "-catch";
const double characterSize = 8d;
@@ -61,128 +61,144 @@ public class WorkflowGraphBuilder(ILogger logger, IYamlSer
public IGraphViewModel Build(WorkflowDefinition workflow)
{
ArgumentNullException.ThrowIfNull(workflow);
+ this.Logger.LogTrace("Starting WorkflowGraphBuilder.Build");
Stopwatch sw = Stopwatch.StartNew();
- var isEmpty = workflow.Do.Count < 1;
var graph = new GraphViewModel();
- //graph.EnableProfiling = true;
- var startNode = this.BuildStartNode(!isEmpty);
+ var startNode = this.BuildStartNode();
var endNode = this.BuildEndNode();
graph.AddNode(startNode);
graph.AddNode(endNode);
- var nextNode = endNode;
- if (!isEmpty)
- {
- nextNode = this.BuildTaskNode(new(workflow, graph, 0, workflow.Do.First().Key, workflow.Do.First().Value, null, "/do", null, endNode, startNode));
- if (nextNode is ClusterViewModel clusterViewModel)
- {
- nextNode = (NodeViewModel)clusterViewModel.AllNodes.Values.First();
- }
- }
- this.BuildEdge(graph, startNode, nextNode);
+ this.BuildTransitions(startNode, new(workflow, graph, workflow.Do, null, null, null, null, "/do", null, startNode, endNode));
sw.Stop();
this.Logger.LogTrace("WorkflowGraphBuilder.Build took {elapsedTime} ms", sw.ElapsedMilliseconds);
return graph;
}
///
- /// Builds a new start
+ /// Gets the anchor used to attach the provided node.
///
- /// A boolean indicating whether or not the node has successor
- /// A new
- protected virtual NodeViewModel BuildStartNode(bool hasSuccessor = false) => new StartNodeViewModel(hasSuccessor);
+ /// The node to get the anchor of
+ /// The type of port the anchor should be
+ /// If the node is a cluster, the corresponding port, the node itself otherwise
+ protected virtual INodeViewModel GetNodeAnchor(INodeViewModel node, NodePortType portType)
+ {
+ if (node is IClusterViewModel cluster)
+ {
+ return portType == NodePortType.Entry ? cluster.Children.First().Value : cluster.Children.Skip(1).First().Value;
+ }
+ return node;
+ }
///
- /// Returns the name, index and reference of the next node
+ /// Gets the identify of the next task for the provided task/transition
///
- /// The rendering context for the task nodes
- /// If true, skips with an if clause
- /// A transition, if different from the context task definition's
- /// The next task
- protected TaskIdentity? GetNextTaskIdentity(TaskNodeRenderingContext context, bool ignoreConditionalTasks, string? transition = null)
+ /// The list of tasks to fetch the next task in
+ /// The current task
+ /// A specific transition, if any (use for switch cases)
+ /// The next task identity
+ protected virtual TaskIdentity GetNextTask(Map tasksList, string? currentTask, string? transition = null)
{
- transition = !string.IsNullOrWhiteSpace(transition) ? transition : context.TaskDefinition.Then;
- if (transition == FlowDirective.End) return null;
- if (transition == FlowDirective.Exit)
+ ArgumentNullException.ThrowIfNull(tasksList);
+ var taskDefinition = tasksList.FirstOrDefault(taskEntry => taskEntry.Key == currentTask)?.Value;
+ transition = !string.IsNullOrWhiteSpace(transition) ? transition : taskDefinition?.Then;
+ if (transition == FlowDirective.End || transition == FlowDirective.Exit)
{
- if (context.ParentContext == null)
- {
- return null;
- }
- return this.GetNextTaskIdentity(context.ParentContext, ignoreConditionalTasks);
+ return new TaskIdentity(transition, -1, null);
+ }
+ int index;
+ if (!string.IsNullOrWhiteSpace(transition) && transition != FlowDirective.Continue)
+ {
+ index = tasksList.Keys.ToList().IndexOf(transition);
}
- var nextTaskName = string.IsNullOrWhiteSpace(transition) || transition == FlowDirective.Continue
- ? context.Workflow.GetTaskAfter(new(context.TaskName, context.TaskDefinition), context.ParentReference, ignoreConditionalTasks)?.Key
- : transition;
- if (string.IsNullOrWhiteSpace(nextTaskName))
+ else if (!string.IsNullOrWhiteSpace(currentTask))
{
- if (context.ParentContext == null)
+ index = tasksList.Keys.ToList().IndexOf(currentTask) + 1;
+ if (index >= tasksList.Count)
{
- return null;
+ return new TaskIdentity(FlowDirective.Exit, -1, null);
}
- return this.GetNextTaskIdentity(context.ParentContext, ignoreConditionalTasks);
}
- var nextTaskIndex = context.Workflow.IndexOf(nextTaskName, context.ParentReference);
- var nextTaskReference = $"{context.ParentReference}/{nextTaskIndex}/{nextTaskName}";
- return new(nextTaskName, nextTaskIndex, nextTaskReference, context);
+ else
+ {
+ index = 0;
+ }
+ var taskEntry = tasksList.ElementAt(index);
+ return new TaskIdentity(taskEntry.Key, index, taskEntry.Value);
}
///
- /// Gets a by reference in the provided
+ /// Builds all possible transitions from the specified node
///
- /// The source
- /// The reference to look for
- ///
- protected NodeViewModel GetNodeByReference(TaskNodeRenderingContext context, string reference)
+ /// The node to transition from
+ /// The rendering context of the provided node
+ protected virtual void BuildTransitions(INodeViewModel node, TaskNodeRenderingContext context)
{
- if (context.Graph.AllClusters.ContainsKey(reference))
+ ArgumentNullException.ThrowIfNull(node);
+ ArgumentNullException.ThrowIfNull(context);
+ this.Logger.LogTrace("Starting WorkflowGraphBuilder.BuildTransitions from '{nodeId}'", node.Id);
+ List transitions = [];
+ TaskIdentity nextTask = this.GetNextTask(context.TasksList, context.TaskName);
+ transitions.Add(nextTask);
+ this.Logger.LogTrace("[WorkflowGraphBuilder.BuildTransitions][{nodeId}] found transition to '{nextTaskName}'", node.Id, nextTask?.Name);
+ while (!string.IsNullOrWhiteSpace(nextTask?.Definition?.If))
{
- return (NodeViewModel)context.Graph.AllClusters[reference].AllNodes.First().Value;
+ this.Logger.LogTrace("[WorkflowGraphBuilder.BuildTransitions][{nodeId}] if clause found, looking up next task.", node.Id);
+ nextTask = this.GetNextTask(context.TasksList, nextTask.Name);
+ transitions.Add(nextTask);
+ this.Logger.LogTrace("[WorkflowGraphBuilder.BuildTransitions][{nodeId}] found transition to '{nextTaskName}'", node.Id, nextTask?.Name);
}
- if (context.Graph.AllNodes.ContainsKey(reference))
+ foreach (var transition in transitions.Distinct(new TaskIdentityComparer()))
{
- return (NodeViewModel)context.Graph.AllNodes[reference];
+ if (transition.Index != -1)
+ {
+ this.Logger.LogTrace("[WorkflowGraphBuilder.BuildTransitions][{nodeId}] Building node '{transitionName}'", node.Id, transition.Name);
+ var transitionNode = this.BuildTaskNode(new(context.Workflow, context.Graph, context.TasksList, transition.Index, transition.Name, transition.Definition, context.TaskGroup, context.ParentReference, context.ParentContext, context.EntryNode, context.ExitNode));
+ this.Logger.LogTrace("[WorkflowGraphBuilder.BuildTransitions][{nodeId}] Building edge to node '{transitionName}'", node.Id, transition.Name);
+ this.BuildEdge(context.Graph, this.GetNodeAnchor(node, NodePortType.Exit), GetNodeAnchor(transitionNode, NodePortType.Entry));
+ }
+ else if (transition.Name == FlowDirective.Exit)
+ {
+ this.Logger.LogTrace("[WorkflowGraphBuilder.BuildTransitions][{nodeId}] Exit transition, building edge to node '{contextExitNode}'", node.Id, context.ExitNode);
+ this.BuildEdge(context.Graph, this.GetNodeAnchor(node, NodePortType.Exit), context.ExitNode);
+ }
+ else if (transition.Name == FlowDirective.End)
+ {
+ this.Logger.LogTrace("[WorkflowGraphBuilder.BuildTransitions][{nodeId}] End transition, building edge to node '{contextExitNode}'", node.Id, context.ExitNode);
+ this.BuildEdge(context.Graph, this.GetNodeAnchor(node, NodePortType.Exit), context.Graph.AllNodes.Skip(1).First().Value);
+ }
+ else
+ {
+ throw new IndexOutOfRangeException("Invalid transition");
+ }
}
- throw new IndexOutOfRangeException($"Unable to find the task with reference '{reference}' in the provided context.");
+ this.Logger.LogTrace("Exiting WorkflowGraphBuilder.BuildTransitions from '{nodeId}'", node.Id);
}
///
- /// Gets the next in the graph
+ /// Builds a new start
///
- /// The rendering context for the task nodes
- /// The current task node
- /// If true, skips with an if clause
- /// A transition, if different from the context task definition's
- /// The next task
- ///
- protected NodeViewModel GetNextNode(TaskNodeRenderingContext context, NodeViewModel currentNode, bool ignoreConditionalTasks = false, string? transition = null)
- {
- var nextTaskIdentity = this.GetNextTaskIdentity(context, ignoreConditionalTasks, transition);
- if (nextTaskIdentity == null)
- {
- return context.EndNode;
- }
- var nextTask = context.Workflow.GetComponent(nextTaskIdentity.Reference) ?? throw new Exception($"Failed to find the task at '{nextTaskIdentity.Reference}' in workflow '{context.Workflow.Document.Name}.{context.Workflow.Document.Namespace}:{context.Workflow.Document.Version}'");
- if (!context.Graph.AllNodes.ContainsKey(nextTaskIdentity.Reference) && !context.Graph.AllClusters.ContainsKey(nextTaskIdentity.Reference))
- {
- this.BuildTaskNode(new(nextTaskIdentity.Context.Workflow, nextTaskIdentity.Context.Graph, nextTaskIdentity.Index, nextTaskIdentity.Name, nextTask, nextTaskIdentity.Context.TaskGroup, nextTaskIdentity.Context.ParentReference, nextTaskIdentity.Context.ParentContext, nextTaskIdentity.Context.EndNode, currentNode));
- }
- if (string.IsNullOrEmpty(nextTask.If))
- {
- return this.GetNodeByReference(context, nextTaskIdentity.Reference);
- }
- var nextNode = this.GetNodeByReference(context, nextTaskIdentity.Reference);
- this.BuildEdge(context.Graph, currentNode, nextNode);
- return this.GetNextNode(context, currentNode, true, transition);
- }
+ /// A new
+ protected virtual NodeViewModel BuildStartNode() => new StartNodeViewModel();
///
/// Builds a new for the specified task
///
/// The rendering context for the task node
/// A new
- protected NodeViewModel BuildTaskNode(TaskNodeRenderingContext context)
+ protected INodeViewModel BuildTaskNode(TaskNodeRenderingContext context)
{
ArgumentNullException.ThrowIfNull(context);
+ this.Logger.LogTrace("Starting WorkflowGraphBuilder.BuildTaskNode for '{contextTaskName}'", context.TaskName);
+ if (context.Graph.AllNodes.ContainsKey(context.TaskReference))
+ {
+ this.Logger.LogTrace("Exiting WorkflowGraphBuilder.BuildTaskNode for '{contextTaskName}', found existing node.", context.TaskName);
+ return context.Graph.AllNodes[context.TaskReference];
+ }
+ if (context.Graph.AllClusters.ContainsKey(context.TaskReference))
+ {
+ this.Logger.LogTrace("Exiting WorkflowGraphBuilder.BuildTaskNode for '{contextTaskName}', found existing cluster.", context.TaskName);
+ return context.Graph.AllClusters[context.TaskReference];
+ }
return context.TaskDefinition switch
{
CallTaskDefinition => this.BuildCallTaskNode(context.OfType()),
@@ -198,7 +214,7 @@ protected NodeViewModel BuildTaskNode(TaskNodeRenderingContext context)
SwitchTaskDefinition => this.BuildSwitchTaskNode(context.OfType()),
TryTaskDefinition => this.BuildTryTaskNode(context.OfType()),
WaitTaskDefinition => this.BuildWaitTaskNode(context.OfType()),
- _ => throw new NotSupportedException($"The specified task type '{context.TaskDefinition.GetType()}' is not supported")
+ _ => throw new NotSupportedException($"The specified task type '{context.TaskDefinition?.GetType()}' is not supported")
} ?? throw new Exception($"Unable to define a last node for task '{context.TaskName}'");
}
@@ -243,13 +259,13 @@ protected virtual NodeViewModel BuildCallTaskNode(TaskNodeRenderingContext 1 ? "s" : "")}");
- var port = new PortNodeViewModel(context.TaskReference + _portSuffix);
- cluster.AddChild(port);
+ var cluster = new DoTaskNodeViewModel(context.TaskReference, context.TaskName!, $"{taskCount} task{(taskCount > 1 ? "s" : "")}");
+ var entryPort = new PortNodeViewModel(context.TaskReference + _clusterEntrySuffix);
+ var exitPort = new PortNodeViewModel(context.TaskReference + _clusterExitSuffix);
+ cluster.AddChild(entryPort);
+ cluster.AddChild(exitPort);
if (context.TaskGroup == null) context.Graph.AddCluster(cluster);
else context.TaskGroup.AddChild(cluster);
- var innerContext = new TaskNodeRenderingContext(context.Workflow, context.Graph, 0, context.TaskDefinition.Do.First().Key, context.TaskDefinition.Do.First().Value, cluster, context.TaskReference + "/do", context, context.EndNode, context.PreviousNode);
- this.BuildTaskNode(innerContext);
- if (taskCount > 0)
- {
- var firstDoNode = (NodeViewModel)cluster.AllNodes.Skip(1).First().Value;
- this.BuildEdge(context.Graph, port, firstDoNode);
- if (taskCount > 1 && !string.IsNullOrWhiteSpace(context.TaskDefinition.Do.First().Value.If))
- {
- this.BuildEdge(context.Graph, port, this.GetNextNode(innerContext, firstDoNode));
- }
- }
- else
- {
- this.BuildEdge(context.Graph, port, this.GetNextNode(context, cluster));
- }
+ var innerContext = new TaskNodeRenderingContext(context.Workflow, context.Graph, context.TaskDefinition.Do, null, null, null, cluster, context.TaskReference + "/do", context, entryPort, exitPort);
+ this.BuildTransitions(entryPort, innerContext);
+ this.BuildTransitions(cluster, context);
return cluster;
}
@@ -293,10 +299,10 @@ protected virtual NodeViewModel BuildDoTaskNode(TaskNodeRenderingContext context)
{
ArgumentNullException.ThrowIfNull(context);
- var node = new EmitTaskNodeViewModel(context.TaskReference, context.TaskName, this.YamlSerializer.SerializeToText(context.TaskDefinition.Emit.Event.With));
+ var node = new EmitTaskNodeViewModel(context.TaskReference, context.TaskName!, this.YamlSerializer.SerializeToText(context.TaskDefinition.Emit.Event.With));
if (context.TaskGroup == null) context.Graph.AddNode(node);
else context.TaskGroup.AddChild(node);
- this.BuildEdge(context.Graph, node, this.GetNextNode(context, node));
+ this.BuildTransitions(node, context);
return node;
}
@@ -308,10 +314,10 @@ protected virtual NodeViewModel BuildEmitTaskNode(TaskNodeRenderingContext context)
{
ArgumentNullException.ThrowIfNull(context);
- var node = new ExtensionTaskNodeViewModel(context.TaskReference, context.TaskName);
+ var node = new ExtensionTaskNodeViewModel(context.TaskReference, context.TaskName!);
if (context.TaskGroup == null) context.Graph.AddNode(node);
else context.TaskGroup.AddChild(node);
- this.BuildEdge(context.Graph, node, this.GetNextNode(context, node));
+ this.BuildTransitions(node, context);
return node;
}
@@ -323,20 +329,16 @@ protected virtual NodeViewModel BuildExtensionTaskNode(TaskNodeRenderingContext<
protected virtual NodeViewModel BuildForTaskNode(TaskNodeRenderingContext context)
{
ArgumentNullException.ThrowIfNull(context);
- var cluster = new ForTaskNodeViewModel(context.TaskReference, context.TaskName, this.YamlSerializer.SerializeToText(context.TaskDefinition.For));
- var port = new PortNodeViewModel(context.TaskReference + _portSuffix);
- cluster.AddChild(port);
+ var cluster = new ForTaskNodeViewModel(context.TaskReference, context.TaskName!, this.YamlSerializer.SerializeToText(context.TaskDefinition.For));
+ var entryPort = new PortNodeViewModel(context.TaskReference + _clusterEntrySuffix);
+ var exitPort = new PortNodeViewModel(context.TaskReference + _clusterExitSuffix);
+ cluster.AddChild(entryPort);
+ cluster.AddChild(exitPort);
if (context.TaskGroup == null) context.Graph.AddCluster(cluster);
else context.TaskGroup.AddChild(cluster);
- this.BuildTaskNode(new(context.Workflow, context.Graph, 0, context.TaskDefinition.Do.First().Key, context.TaskDefinition.Do.First().Value, cluster, context.TaskReference + "/do", context, context.EndNode, context.PreviousNode));
- if (context.TaskDefinition.Do.Count > 0)
- {
- this.BuildEdge(context.Graph, port, (NodeViewModel)cluster.AllNodes.Skip(1).First().Value);
- }
- else
- {
- this.BuildEdge(context.Graph, port, this.GetNextNode(context, cluster));
- }
+ var innerContext = new TaskNodeRenderingContext(context.Workflow, context.Graph, context.TaskDefinition.Do, null, null, null, cluster, context.TaskReference + "/do", context, entryPort, exitPort);
+ this.BuildTransitions(entryPort, innerContext);
+ this.BuildTransitions(cluster, context);
return cluster;
}
@@ -348,16 +350,17 @@ protected virtual NodeViewModel BuildForTaskNode(TaskNodeRenderingContext context)
{
ArgumentNullException.ThrowIfNull(context);
- var cluster = new ForkTaskNodeViewModel(context.TaskReference, context.TaskName, this.YamlSerializer.SerializeToText(context.TaskDefinition.Fork));
- var entryPort = new PortNodeViewModel(context.TaskReference + _portSuffix);
- var exitPort = new PortNodeViewModel(context.TaskReference + "-exit" + _portSuffix);
+ var cluster = new ForkTaskNodeViewModel(context.TaskReference, context.TaskName!, this.YamlSerializer.SerializeToText(context.TaskDefinition.Fork));
+ var entryPort = new PortNodeViewModel(context.TaskReference + _clusterEntrySuffix);
+ var exitPort = new PortNodeViewModel(context.TaskReference + _clusterExitSuffix);
cluster.AddChild(entryPort);
+ cluster.AddChild(exitPort);
if (context.TaskGroup == null) context.Graph.AddCluster(cluster);
else context.TaskGroup.AddChild(cluster);
- for (int i = 0, c = context.TaskDefinition.Fork.Branches.Count; i context)
{
ArgumentNullException.ThrowIfNull(context);
- var node = new ListenTaskNodeViewModel(context.TaskReference, context.TaskName, this.YamlSerializer.SerializeToText(context.TaskDefinition.Listen));
+ var node = new ListenTaskNodeViewModel(context.TaskReference, context.TaskName!, this.YamlSerializer.SerializeToText(context.TaskDefinition.Listen));
if (context.TaskGroup == null) context.Graph.AddNode(node);
else context.TaskGroup.AddChild(node);
- this.BuildEdge(context.Graph, node, this.GetNextNode(context, node));
+ this.BuildTransitions(node, context);
return node;
}
@@ -397,10 +399,10 @@ protected virtual NodeViewModel BuildListenTaskNode(TaskNodeRenderingContext context)
{
ArgumentNullException.ThrowIfNull(context);
- var node = new RaiseTaskNodeViewModel(context.TaskReference, context.TaskName, this.YamlSerializer.SerializeToText(context.TaskDefinition.Raise.Error));
+ var node = new RaiseTaskNodeViewModel(context.TaskReference, context.TaskName!, this.YamlSerializer.SerializeToText(context.TaskDefinition.Raise.Error));
if (context.TaskGroup == null) context.Graph.AddNode(node);
else context.TaskGroup.AddChild(node);
- this.BuildEdge(context.Graph, node, this.GetNextNode(context, node));
+ this.BuildTransitions(node, context);
return node;
}
@@ -444,10 +446,10 @@ protected virtual NodeViewModel BuildRunTaskNode(TaskNodeRenderingContext context)
{
ArgumentNullException.ThrowIfNull(context);
- var node = new SetTaskNodeViewModel(context.TaskReference, context.TaskName, this.YamlSerializer.SerializeToText(context.TaskDefinition.Set));
+ var node = new SetTaskNodeViewModel(context.TaskReference, context.TaskName!, this.YamlSerializer.SerializeToText(context.TaskDefinition.Set));
if (context.TaskGroup == null) context.Graph.AddNode(node);
else context.TaskGroup.AddChild(node);
- this.BuildEdge(context.Graph, node, this.GetNextNode(context, node));
+ this.BuildTransitions(node, context);
return node;
}
@@ -474,17 +476,18 @@ protected virtual NodeViewModel BuildSetTaskNode(TaskNodeRenderingContext context)
{
ArgumentNullException.ThrowIfNull(context);
- var node = new SwitchTaskNodeViewModel(context.TaskReference, context.TaskName, this.YamlSerializer.SerializeToText(context.TaskDefinition.Switch));
+ var node = new SwitchTaskNodeViewModel(context.TaskReference, context.TaskName!, this.YamlSerializer.SerializeToText(context.TaskDefinition.Switch));
if (context.TaskGroup == null) context.Graph.AddNode(node);
else context.TaskGroup.AddChild(node);
foreach (var switchCase in context.TaskDefinition.Switch)
{
- var switchCaseNode = this.GetNextNode(context, node, false, switchCase.Value.Then);
- this.BuildEdge(context.Graph, node, switchCaseNode, switchCase.Key);
+ var switchCaseTask = this.GetNextTask(context.TasksList, context.TaskName, switchCase.Value.Then)!;
+ var switchCaseNode = this.BuildTaskNode(new(context.Workflow, context.Graph, context.TasksList, switchCaseTask.Index, switchCaseTask.Name, switchCaseTask.Definition, context.TaskGroup, context.ParentReference, context.ParentContext, context.EntryNode, context.ExitNode));
+ this.BuildEdge(context.Graph, this.GetNodeAnchor(node, NodePortType.Exit), GetNodeAnchor(switchCaseNode, NodePortType.Entry));
}
if (!context.TaskDefinition.Switch.Any(switchCase => string.IsNullOrEmpty(switchCase.Value.When)))
{
- this.BuildEdge(context.Graph, node, this.GetNextNode(context, node));
+ this.BuildTransitions(node, context);
}
return node;
}
@@ -498,47 +501,46 @@ protected virtual NodeViewModel BuildTryTaskNode(TaskNodeRenderingContext 1 ? "s" : "")}");
- var containerPort = new PortNodeViewModel(context.TaskReference + _portSuffix);
- containerCluster.AddChild(containerPort);
+ var containerCluster = new TryTaskNodeViewModel(context.TaskReference, context.TaskName!, $"{taskCount} task{(taskCount > 1 ? "s" : "")}");
+ var containerEntryPort = new PortNodeViewModel(context.TaskReference + _clusterEntrySuffix);
+ var containerExitPort = new PortNodeViewModel(context.TaskReference + _clusterExitSuffix);
+ containerCluster.AddChild(containerEntryPort);
+ containerCluster.AddChild(containerExitPort);
if (context.TaskGroup == null) context.Graph.AddCluster(containerCluster);
else context.TaskGroup.AddChild(containerCluster);
- var tryCluster = new TryNodeViewModel(context.TaskReference + _trySuffix, context.TaskName, string.Empty);
- var tryPort = new PortNodeViewModel(context.TaskReference + _trySuffix + _portSuffix);
- tryCluster.AddChild(tryPort);
+ var tryCluster = new TryNodeViewModel(context.TaskReference + _trySuffix, context.TaskName!, string.Empty);
+ var tryEntryPort = new PortNodeViewModel(context.TaskReference + _trySuffix + _clusterEntrySuffix);
+ var tryExitPort = new PortNodeViewModel(context.TaskReference + _trySuffix + _clusterExitSuffix);
+ tryCluster.AddChild(tryEntryPort);
+ tryCluster.AddChild(tryExitPort);
containerCluster.AddChild(tryCluster);
- this.BuildEdge(context.Graph, containerPort, tryPort);
- var innerContext = new TaskNodeRenderingContext(context.Workflow, context.Graph, 0, context.TaskDefinition.Try.First().Key, context.TaskDefinition.Try.First().Value, tryCluster, context.TaskReference + "/try", context, context.EndNode, context.PreviousNode);
- this.BuildTaskNode(innerContext);
- if (taskCount > 0)
- {
- var firstNode = (NodeViewModel)tryCluster.AllNodes.Skip(1).First().Value;
- this.BuildEdge(context.Graph, tryPort, firstNode);
- if (taskCount > 1 && !string.IsNullOrWhiteSpace(context.TaskDefinition.Try.First().Value.If))
- {
- this.BuildEdge(context.Graph, tryPort, this.GetNextNode(innerContext, firstNode));
- }
- }
+ this.BuildEdge(context.Graph, containerEntryPort, tryEntryPort);
+ var innerContext = new TaskNodeRenderingContext(context.Workflow, context.Graph, context.TaskDefinition.Try, null, null, null, tryCluster, context.TaskReference + "/try", context, tryEntryPort, tryExitPort);
+ this.BuildTransitions(tryEntryPort, innerContext);
+
var catchContent = this.YamlSerializer.SerializeToText(context.TaskDefinition.Catch);
if (context.TaskDefinition.Catch.Do == null || context.TaskDefinition.Catch.Do.Count == 0)
{
- var catchNode = new CatchNodeViewModel(context.TaskReference + _catchSuffix, context.TaskName, catchContent);
+ var catchNode = new CatchNodeViewModel(context.TaskReference + _catchSuffix, context.TaskName!, catchContent);
containerCluster.AddChild(catchNode);
- this.BuildEdge(context.Graph, tryCluster.AllNodes.Values.Last(), catchNode);
- this.BuildEdge(context.Graph, catchNode, this.GetNextNode(context, containerCluster));
+ this.BuildEdge(context.Graph, tryExitPort, catchNode);
+ this.BuildEdge(context.Graph, catchNode, containerExitPort);
}
else
{
- var catchCluster = new CatchDoNodeViewModel(context.TaskReference + _catchSuffix, context.TaskName, catchContent);
- var catchPort = new PortNodeViewModel(context.TaskReference + _catchSuffix + _portSuffix);
- catchCluster.AddChild(catchPort);
+ var catchCluster = new CatchDoNodeViewModel(context.TaskReference + _catchSuffix, context.TaskName!, catchContent);
+ var catchEntryPort = new PortNodeViewModel(context.TaskReference + _catchSuffix + _clusterEntrySuffix);
+ var catchExitPort = new PortNodeViewModel(context.TaskReference + _catchSuffix + _clusterExitSuffix);
+ catchCluster.AddChild(catchEntryPort);
+ catchCluster.AddChild(catchExitPort);
containerCluster.AddChild(catchCluster);
- this.BuildEdge(context.Graph, tryCluster.AllNodes.Values.Last(), catchPort);
- this.BuildTaskNode(new(context.Workflow, context.Graph, 0, context.TaskDefinition.Catch.Do.First().Key, context.TaskDefinition.Catch.Do.First().Value, catchCluster, context.TaskReference + "/catch/do", context, context.EndNode, context.PreviousNode));
- this.BuildEdge(context.Graph, catchPort, catchCluster.AllNodes.Values.Skip(1).First());
- this.BuildEdge(context.Graph, catchCluster.AllNodes.Values.Last(), this.GetNextNode(context, containerCluster));
+ this.BuildEdge(context.Graph, tryExitPort, catchEntryPort);
+ var catchContext = new TaskNodeRenderingContext(context.Workflow, context.Graph, context.TaskDefinition.Catch.Do, null, null, null, catchCluster, context.TaskReference + "/catch/do", context, catchEntryPort, catchExitPort);
+ this.BuildTransitions(catchEntryPort, catchContext);
+ this.BuildEdge(context.Graph, catchExitPort, containerExitPort);
}
+ this.BuildTransitions(containerCluster, context);
return containerCluster;
}
@@ -550,10 +552,10 @@ protected virtual NodeViewModel BuildTryTaskNode(TaskNodeRenderingContext context)
{
ArgumentNullException.ThrowIfNull(context);
- var node = new WaitTaskNodeViewModel(context.TaskReference, context.TaskName, context.TaskDefinition.Wait.ToTimeSpan().ToString("hh\\:mm\\:ss\\.fff"));
+ var node = new WaitTaskNodeViewModel(context.TaskReference, context.TaskName!, context.TaskDefinition.Wait.ToTimeSpan().ToString("hh\\:mm\\:ss\\.fff"));
if (context.TaskGroup == null) context.Graph.AddNode(node);
else context.TaskGroup.AddChild(node);
- this.BuildEdge(context.Graph, node, this.GetNextNode(context, node));
+ this.BuildTransitions(node, context);
return node;
}
@@ -576,7 +578,8 @@ protected virtual IEdgeViewModel BuildEdge(IGraphViewModel graph, INodeViewModel
var edge = graph.Edges.Select(keyValuePair => keyValuePair.Value).FirstOrDefault(edge => edge.SourceId == source.Id && edge.TargetId == target.Id);
if (edge != null)
{
- if (!string.IsNullOrEmpty(label)) {
+ if (!string.IsNullOrEmpty(label))
+ {
edge.Label = edge.Label + " / " + label;
edge.Width = edge.Label.Length * characterSize;
}
@@ -588,7 +591,7 @@ protected virtual IEdgeViewModel BuildEdge(IGraphViewModel graph, INodeViewModel
edge.LabelPosition = EdgeLabelPosition.Center;
edge.Width = edge.Label.Length * characterSize;
}
- if (target.Id.EndsWith(_portSuffix))
+ if (target.Id.EndsWith(_clusterEntrySuffix) || target.Id.EndsWith(_clusterExitSuffix))
{
edge.EndMarkerId = null;
}
@@ -600,15 +603,16 @@ protected virtual IEdgeViewModel BuildEdge(IGraphViewModel graph, INodeViewModel
///
/// The workflow definition.
/// The graph view model.
+ /// The list of tasks in the rendering context.
/// The index of the task.
/// The name of the task.
/// The definition of the task.
/// The optional task group.
/// The reference to the parent task node.
/// The parent rendering context of the task node.
- /// The end node view model.
- /// The previous node view model.
- protected class TaskNodeRenderingContext(WorkflowDefinition workflow, GraphViewModel graph, int taskIndex, string taskName, TaskDefinition taskDefinition, WorkflowClusterViewModel? taskGroup, string parentReference, TaskNodeRenderingContext? parentContext, NodeViewModel endNode, NodeViewModel previousNode)
+ /// The entry node view model of the context.
+ /// The exit node view model of the context.
+ protected class TaskNodeRenderingContext(WorkflowDefinition workflow, GraphViewModel graph, Map tasksList, int? taskIndex, string? taskName, TaskDefinition? taskDefinition, WorkflowClusterViewModel? taskGroup, string parentReference, TaskNodeRenderingContext? parentContext, NodeViewModel entryNode, NodeViewModel exitNode)
{
///
@@ -621,20 +625,25 @@ protected class TaskNodeRenderingContext(WorkflowDefinition workflow, GraphViewM
///
public virtual GraphViewModel Graph { get; } = graph;
+ ///
+ /// Gets the list of tasks in the rendering context.
+ ///
+ public virtual Map TasksList { get; } = tasksList;
+
///
/// Gets the index of the task.
///
- public virtual int TaskIndex { get; } = taskIndex;
+ public virtual int? TaskIndex { get; } = taskIndex;
///
/// Gets the name of the task.
///
- public virtual string TaskName { get; } = taskName;
+ public virtual string? TaskName { get; } = taskName;
///
/// Gets the definition of the task.
///
- public virtual TaskDefinition TaskDefinition { get; } = taskDefinition;
+ public virtual TaskDefinition? TaskDefinition { get; } = taskDefinition;
///
/// Gets the optional task group.
@@ -657,21 +666,22 @@ protected class TaskNodeRenderingContext(WorkflowDefinition workflow, GraphViewM
public virtual TaskNodeRenderingContext? ParentContext { get; } = parentContext;
///
- /// Gets the end node view model.
+ /// Gets the entry node view model.
///
- public virtual NodeViewModel EndNode { get; } = endNode;
+ public virtual NodeViewModel EntryNode { get; } = entryNode;
///
- /// Gets the previous node view model.
+ /// Gets the exit node view model.
///
- public virtual NodeViewModel PreviousNode { get; } = previousNode;
+ public virtual NodeViewModel ExitNode { get; } = exitNode;
+
///
/// Creates a new instance of with the specified task definition type.
///
/// The type of the task definition.
/// A new instance of .
- public virtual TaskNodeRenderingContext OfType() where TDefinition : TaskDefinition => new(this.Workflow, this.Graph, this.TaskIndex, this.TaskName, this.TaskDefinition, this.TaskGroup, this.ParentReference, this.ParentContext, this.EndNode, this.PreviousNode);
+ public virtual TaskNodeRenderingContext OfType() where TDefinition : TaskDefinition => new(this.Workflow, this.Graph, this.TasksList, this.TaskIndex, this.TaskName, this.TaskDefinition, this.TaskGroup, this.ParentReference, this.ParentContext, this.EntryNode, this.ExitNode);
}
@@ -681,52 +691,71 @@ protected class TaskNodeRenderingContext(WorkflowDefinition workflow, GraphViewM
/// The type of the task definition.
/// The workflow definition.
/// The graph view model.
+ /// The list of tasks in the rendering context.
/// The index of the task.
/// The name of the task.
/// The definition of the task.
/// The optional task group.
/// The reference to the parent task node.
/// The parent rendering context of the task node.
- /// The end node view model.
- /// The previous node view model.
- protected class TaskNodeRenderingContext(WorkflowDefinition workflow, GraphViewModel graph, int taskIndex, string taskName, TaskDefinition taskDefinition, WorkflowClusterViewModel? taskGroup, string parentReference, TaskNodeRenderingContext? parentContext, NodeViewModel endNode, NodeViewModel previousNode)
- : TaskNodeRenderingContext(workflow, graph, taskIndex, taskName, taskDefinition, taskGroup, parentReference, parentContext, endNode, previousNode)
+ /// The end node view model.
+ /// The previous node view model.
+ protected class TaskNodeRenderingContext(WorkflowDefinition workflow, GraphViewModel graph, Map tasksList, int? taskIndex, string? taskName, TaskDefinition? taskDefinition, WorkflowClusterViewModel? taskGroup, string parentReference, TaskNodeRenderingContext? parentContext, NodeViewModel entryNode, NodeViewModel exitNode)
+ : TaskNodeRenderingContext(workflow, graph, tasksList, taskIndex, taskName, taskDefinition, taskGroup, parentReference, parentContext, entryNode, exitNode)
where TDefinition : TaskDefinition
{
///
/// Gets the task definition of type .
///
- public new virtual TDefinition TaskDefinition => (TDefinition)base.TaskDefinition;
+ public new virtual TDefinition TaskDefinition => (TDefinition)base.TaskDefinition!;
}
///
/// Represents the identity of a task
///
- /// The task name
- /// The task index
- /// The task reference
- /// The task rendering context
- protected class TaskIdentity(string name, int index, string reference, TaskNodeRenderingContext context)
+ /// The task name
+ /// The task index
+ /// The task definition
+ protected record TaskIdentity(string Name, int Index, TaskDefinition? Definition)
{
- ///
- /// Gets the task name
- ///
- public string Name { get; } = name;
+ }
+ ///
+ /// Represents a port type
+ ///
+ protected enum NodePortType
+ {
///
- /// Gets the task index
+ /// The entry port of a cluster
///
- public int Index { get; } = index;
-
+ Entry = 0,
///
- /// Gets the task reference
+ /// The exit port of a cluster
///
- public string Reference { get; } = reference;
+ Exit = 1
+ }
- ///
- /// Get the task rendering context
- ///
- public TaskNodeRenderingContext Context { get; } = context;
+ ///
+ /// The object used to compare
+ ///
+ protected class TaskIdentityComparer : IEqualityComparer
+ {
+ ///
+ public bool Equals(TaskIdentity? identity1, TaskIdentity? identity2)
+ {
+ if (ReferenceEquals(identity1, identity2))
+ return true;
+
+ if (identity1 is null || identity2 is null)
+ return false;
+
+ return identity1.Name == identity2.Name &&
+ identity1.Index == identity2.Index &&
+ identity1.Definition == identity2.Definition;
+ }
+
+ ///
+ public int GetHashCode(TaskIdentity identity) => identity.Name.GetHashCode();
}
}
diff --git a/src/dashboard/Synapse.Dashboard/Synapse.Dashboard.csproj b/src/dashboard/Synapse.Dashboard/Synapse.Dashboard.csproj
index 2eedd46e..8d30a806 100644
--- a/src/dashboard/Synapse.Dashboard/Synapse.Dashboard.csproj
+++ b/src/dashboard/Synapse.Dashboard/Synapse.Dashboard.csproj
@@ -17,7 +17,7 @@
-
+
diff --git a/src/runner/Synapse.Runner/Synapse.Runner.csproj b/src/runner/Synapse.Runner/Synapse.Runner.csproj
index f9225b9e..3ef64dc1 100644
--- a/src/runner/Synapse.Runner/Synapse.Runner.csproj
+++ b/src/runner/Synapse.Runner/Synapse.Runner.csproj
@@ -59,12 +59,12 @@
-
-
-
-
-
-
+
+
+
+
+
+
diff --git a/tests/Synapse.UnitTests/Synapse.UnitTests.csproj b/tests/Synapse.UnitTests/Synapse.UnitTests.csproj
index f66705d3..8952fcaf 100644
--- a/tests/Synapse.UnitTests/Synapse.UnitTests.csproj
+++ b/tests/Synapse.UnitTests/Synapse.UnitTests.csproj
@@ -19,9 +19,9 @@
-
-
-
+
+
+