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 @@ - - - + + +