Skip to content
Merged
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;
Comment thread
handreyrc marked this conversation as resolved.

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
@@ -1,5 +1,101 @@
// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html

exports[`buildGraph > returns a loaded extended graph object from model 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",
"type": "start",
},
"exitNode": {
"id": "root-exit-node",
"type": "end",
},
"id": "root",
"label": undefined,
"nodes": [
{
"id": "root-entry-node",
"type": "start",
},
{
"id": "root-exit-node",
"type": "end",
},
{
"id": "/do/0/step1",
"label": "step1",
"type": "set",
},
{
"id": "/do/1/step2",
"label": "step2",
"type": "set",
},
{
"id": "/do/2/step3",
"label": "step3",
"type": "set",
},
{
"id": "/do/3/step4",
"label": "step4",
"type": "set",
},
{
"id": "/do/4/step5",
"label": "step5",
"type": "set",
},
],
"parent": undefined,
"type": "root",
}
`;

exports[`parseWorkflow > parses valid 'JSON' and returns model with no errors 1`] = `
{
"errors": [],
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
/*
* 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 { describe, it, expect } from "vitest";
import { applyAutoLayout, buildGraph, parseWorkflow } from "../../src/core";
import { BASIC_VALID_WORKFLOW_JSON_TASKS } from "../fixtures/workflows";

describe("applyAutoLayout", () => {
it("apply auto-layout calculated layout to graph elements", () => {
const result = parseWorkflow(BASIC_VALID_WORKFLOW_JSON_TASKS);

const graph = applyAutoLayout(buildGraph(result.model!));

expect(graph!.nodes).toHaveLength(7);
expect(graph!.edges).toHaveLength(6);

let y = 0;
graph!.nodes.forEach((node) => {
// TODO coordinates are fixed (y = y + 100) for now
expect(node.position!.y).toBe(y);
y += 100;
});
});
});
Loading