Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,12 @@ Core package agnostic from the rendering library and its types.

## Modules

`workflowSdk.ts` and `graph.ts` are the only places in the diagram editor that import from the SDK directly, keeping the rest of the editor decoupled from SDK implementation details.

### workflowSdk.ts

Abstraction layer over the `@serverlessworkflow/sdk`. This is the only place in the diagram editor that imports from the SDK directly keeping the rest of the editor decoupled from SDK implementation details.
Abstraction layer over the `@serverlessworkflow/sdk`.

### graph.ts

Add custom types to the original sdk `Graph` type.
34 changes: 34 additions & 0 deletions packages/serverless-workflow-diagram-editor/src/core/autoLayout.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
/*
* Copyright 2021-Present The Serverless Workflow Specification 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.
*/

import { ExtendedGraph, Position, Size } from "./graph";

export function applyAutoLayout(graph: ExtendedGraph): ExtendedGraph {
const graphClone = structuredClone(graph);

// TODO: This is just a temporary implementation until the actual auto-layout engine is integrated
const nodeSize: Size = { height: 50, width: 70 };
let position: Position = { x: 0, y: 0 };

// TODO: Containment is not supported for now.
graphClone.nodes.forEach((node) => {
node.size = { ...nodeSize };
node.position = { ...position };
position.y = position.y + 100;
});

return graphClone;
}
107 changes: 107 additions & 0 deletions packages/serverless-workflow-diagram-editor/src/core/graph.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,107 @@
/*
* Copyright 2021-Present The Serverless Workflow Specification 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.
*/

import { Graph, GraphEdge, GraphNode, GraphNodeType } from "@serverlessworkflow/sdk";

// Override / add multiple properties of a type in a generic way
export type Override<T, NewProps> = Omit<T, keyof NewProps> & NewProps;

// Supported edge types
export enum GraphEdgeType {
Default = "default",
Error = "error",
Condition = "condition",
}

export type Point = {
x: number;
y: number;
};

export type Position = Point;

export type Size = {
height: number;
width: number;
};

export type WayPoints = Point[];

// Add extra properties to GraphNode
export type ExtendedGraphNode = Override<
GraphNode,
{
position?: Position;
size?: Size;
}
>;

// Add extra properties to GraphEdge
export type ExtendedGraphEdge = GraphEdge & {
type?: GraphEdgeType;
wayPoints?: WayPoints;
};

export type ExtendedGraph = Override<
Graph,
{
parent?: ExtendedGraph | null;
nodes: ExtendedGraphNode[];
edges: ExtendedGraphEdge[];
entryNode: ExtendedGraphNode;
exitNode: ExtendedGraphNode;
}
>;

export function solveEdgeTypes(graph: ExtendedGraph): ExtendedGraph {
const graphClone = structuredClone(graph);

// root level
setEdgeTypes(graphClone);
// children n level
graphClone.nodes.flat().forEach((node) => setEdgeTypes(node as ExtendedGraph));

return graphClone;
}

Comment thread
handreyrc marked this conversation as resolved.
function setEdgeTypes(graph: ExtendedGraph): ExtendedGraph {
if (!graph.edges || !graph.nodes) {
return graph;
}

for (let i = 0; i < graph.nodes.length; i++) {
const graphNode = graph.nodes[i]! as ExtendedGraph;
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In this point we are copying a memory reference, because graph.nodes[i]! is an object.
In general, as a good practice, we should clone the input without changing it.
Do you agree?

Copy link
Copy Markdown
Contributor Author

@handreyrc handreyrc Apr 18, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@fantonangeli,

The goal of the 'solveEdgeTypes' function is to traverse a brand new graph created by the sdk.buildGraph to set properties. As you can see 'solveEdgeTypes' is and must only be called from within the buildGraph wrapper in the workflowSdk.ts. It does not make sense to call it unless you have a brand new graph instance created straight from the skd, so I don't see any risk of messing with references elsewhere.
I exported 'solveEdgeTypes' in order to write unit tests for it.
If we really want to make a graph clone to work with, we should use lodash to create a deepclone before calling the recursive function 'solveEdgeTypes'. The graph is a pretty complex object to navigate so it may go prety deep and there are circular references (parent refs) to be handled.

WDYT?

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@handreyrc I tried locally using https://developer.mozilla.org/en-US/docs/Web/API/Window/structuredClone#browser_compatibility , which is a native deep clone.
TBH I never used it before, but it passed the tests.
Anyway there can be a bit of performance slow down with big graphs and I can also agree with your point:
"The goal of the 'solveEdgeTypes' function is to traverse a brand new graph created by the sdk.buildGraph to set properties"

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@fantonangeli,

Thanks for the tip! The structuredClone worked well!
Fixed!


for (let j = 0; j < graph.edges.length; j++) {
const graphEdge = graph.edges[j]!;

if (graphNode.id === graphEdge.sourceId) {
switch (graphNode.type) {
case GraphNodeType.Raise:
graphEdge.type = GraphEdgeType.Error;
break;
case GraphNodeType.Switch:
graphEdge.type = GraphEdgeType.Condition;
break;
Comment thread
handreyrc marked this conversation as resolved.
default:
graphEdge.type = GraphEdgeType.Default;
}
}
}
Comment thread
handreyrc marked this conversation as resolved.
Comment thread
handreyrc marked this conversation as resolved.
}

return graph;
}
2 changes: 2 additions & 0 deletions packages/serverless-workflow-diagram-editor/src/core/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,3 +15,5 @@
*/

export * from "./workflowSdk";
export * from "./graph";
export * from "./autoLayout";
Original file line number Diff line number Diff line change
Expand Up @@ -15,16 +15,17 @@
*/

import yaml from "js-yaml";
import { Classes, Specification, validate } from "@serverlessworkflow/sdk";
import * as sdk from "@serverlessworkflow/sdk";
import { ExtendedGraph, solveEdgeTypes } from "./graph";

export type WorkflowParseResult = {
model: Specification.Workflow | null;
model: sdk.Specification.Workflow | null;
errors: Error[];
};

export function validateWorkflow(model: Specification.Workflow): Error[] {
export function validateWorkflow(model: sdk.Specification.Workflow): Error[] {
try {
validate("Workflow", model);
sdk.validate("Workflow", model);
return [];
} catch (err) {
// TODO: Parse individual validation errors from the SDK into separate Error objects when we are ready to render them.
Expand All @@ -33,10 +34,10 @@ export function validateWorkflow(model: Specification.Workflow): Error[] {
}

export function parseWorkflow(text: string): WorkflowParseResult {
let raw: Partial<Specification.Workflow>;
let raw: Partial<sdk.Specification.Workflow>;

try {
raw = yaml.load(text, { schema: yaml.DEFAULT_SCHEMA }) as Partial<Specification.Workflow>;
raw = yaml.load(text, { schema: yaml.DEFAULT_SCHEMA }) as Partial<sdk.Specification.Workflow>;
} catch (err) {
return {
model: null,
Expand All @@ -48,8 +49,12 @@ export function parseWorkflow(text: string): WorkflowParseResult {
return { model: null, errors: [new Error("Not a valid workflow object")] };
}

const model = new Classes.Workflow(raw) as Specification.Workflow;
const model = new sdk.Classes.Workflow(raw) as sdk.Specification.Workflow;
const errors = validateWorkflow(model);

return { model, errors };
}

export function buildGraph(model: sdk.Specification.Workflow): ExtendedGraph {
Comment thread
handreyrc marked this conversation as resolved.
return solveEdgeTypes(sdk.buildGraph(model));
Comment thread
handreyrc marked this conversation as resolved.
Comment thread
handreyrc marked this conversation as resolved.
}
Comment thread
handreyrc marked this conversation as resolved.
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,6 @@
*/

import * as React from "react";
import type { Specification } from "@serverlessworkflow/sdk";
import { parseWorkflow } from "../core";
import { DiagramEditorProps } from "../diagram-editor/DiagramEditor";
import { DiagramEditorContext, DiagramEditorContextType } from "./DiagramEditorContext";
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,169 @@
// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html

exports[`applyAutoLayout > apply auto-layout calculated layout to graph elements 1`] = `
{
"edges": [
{
"destinationId": "root-exit-node",
"id": "/do/4/step5-root-exit-node",
"label": "",
"sourceId": "/do/4/step5",
"type": "default",
},
{
"destinationId": "/do/4/step5",
"id": "/do/3/step4-/do/4/step5",
"label": "",
"sourceId": "/do/3/step4",
"type": "default",
},
{
"destinationId": "/do/3/step4",
"id": "/do/2/step3-/do/3/step4",
"label": "",
"sourceId": "/do/2/step3",
"type": "default",
},
{
"destinationId": "/do/2/step3",
"id": "/do/1/step2-/do/2/step3",
"label": "",
"sourceId": "/do/1/step2",
"type": "default",
},
{
"destinationId": "/do/1/step2",
"id": "/do/0/step1-/do/1/step2",
"label": "",
"sourceId": "/do/0/step1",
"type": "default",
},
{
"destinationId": "/do/0/step1",
"id": "root-entry-node-/do/0/step1",
"label": "",
"sourceId": "root-entry-node",
"type": "default",
},
],
"entryNode": {
"id": "root-entry-node",
"position": {
"x": 0,
"y": 0,
},
"size": {
"height": 50,
"width": 70,
},
"type": "start",
},
"exitNode": {
"id": "root-exit-node",
"position": {
"x": 0,
"y": 100,
},
"size": {
"height": 50,
"width": 70,
},
"type": "end",
},
"id": "root",
"label": undefined,
"nodes": [
{
"id": "root-entry-node",
"position": {
"x": 0,
"y": 0,
},
"size": {
"height": 50,
"width": 70,
},
"type": "start",
},
{
"id": "root-exit-node",
"position": {
"x": 0,
"y": 100,
},
"size": {
"height": 50,
"width": 70,
},
"type": "end",
},
{
"id": "/do/0/step1",
"label": "step1",
"position": {
"x": 0,
"y": 200,
},
"size": {
"height": 50,
"width": 70,
},
"type": "set",
},
{
"id": "/do/1/step2",
"label": "step2",
"position": {
"x": 0,
"y": 300,
},
"size": {
"height": 50,
"width": 70,
},
"type": "set",
},
{
"id": "/do/2/step3",
"label": "step3",
"position": {
"x": 0,
"y": 400,
},
"size": {
"height": 50,
"width": 70,
},
"type": "set",
},
{
"id": "/do/3/step4",
"label": "step4",
"position": {
"x": 0,
"y": 500,
},
"size": {
"height": 50,
"width": 70,
},
"type": "set",
},
{
"id": "/do/4/step5",
"label": "step5",
"position": {
"x": 0,
"y": 600,
},
"size": {
"height": 50,
"width": 70,
},
"type": "set",
},
],
"parent": undefined,
"type": "root",
}
`;
Loading
Loading