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 @@ -19,7 +19,7 @@ import { ExtendedGraph, Position, Size } from "./graph";
// Defaults
export const DEFAULT_NODE_SIZE = {
height: 60,
width: 180,
width: 200,
};

export function applyAutoLayout(graph: ExtendedGraph): ExtendedGraph {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,3 +17,4 @@
export * from "./workflowSdk";
export * from "./graph";
export * from "./autoLayout";
export * from "./taskSubType";
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
/*
* 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 { GraphNodeType, type Specification } from "@serverlessworkflow/sdk";

export function getCallSubType(task: Specification.CallTask): string | undefined {
return typeof task.call === "string" ? task.call : undefined;
}

export function getRunSubType(task: Specification.RunTask): string | undefined {
const run = task.run;
if (run && typeof run === "object") {
const firstKey = Object.keys(run)[0];
return firstKey ?? undefined;
}
return undefined;
}

export function getListenSubType(task: Specification.ListenTask): string | undefined {
const listen = task.listen?.to;
if (listen && typeof listen === "object") {
const firstKey = Object.keys(listen)[0];
return firstKey ?? undefined;
}
return undefined;
}

/* TODO: Add container subtypes when container nodes are available. This is the entry point to be called when we remove hardcoded values in Diagram.tsx */
export function getTaskSubType(nodeType: GraphNodeType, task: Specification.Task): string | undefined {
switch (nodeType) {
case GraphNodeType.Call:
return getCallSubType(task as Specification.CallTask);
case GraphNodeType.Run:
return getRunSubType(task as Specification.RunTask);
case GraphNodeType.Listen:
return getListenSubType(task as Specification.ListenTask);
default:
return undefined;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -198,22 +198,25 @@
@apply dec:flex
dec:items-center
dec:gap-3
dec:px-4
dec:py-3;
dec:px-3
dec:py-2;
}

.dec-root .dec-task-node-icon {
@apply dec:shrink-0;
color: var(--task-node-color);
}

.dec-root .dec-task-node-label {
@apply dec:flex
dec:flex-col
dec:gap-0.5;
dec:gap-0.5
dec:min-w-0;
}

.dec-root .dec-task-node-name {
@apply dec:text-sm
@apply dec:truncate
dec:text-sm
dec:text-black
dec:leading-tight;
}
Expand All @@ -232,4 +235,28 @@
.dec-root.dark .dec-task-node-type {
@apply dec:text-gray-400;
}

.dec-root .dec-task-node-badge {
@apply dec:ml-auto
dec:shrink-0
dec:rounded
dec:px-2
dec:py-0.5
dec:text-[8px]
dec:font-semibold
dec:uppercase
dec:whitespace-nowrap;
color: var(--task-node-color);
border: 1px solid var(--task-node-color);
}

.dec-root .dec-task-node-badge-icon {
@apply dec:ml-auto
dec:shrink-0
dec:flex
dec:items-center
dec:justify-center;
color: var(--task-node-color);
}
/* end task leaf nodes */
}
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,7 @@ const initialNodes: RF.Node[] = [
position: { x: 100, y: 0 },
height: DEFAULT_NODE_SIZE.height,
width: DEFAULT_NODE_SIZE.width,
data: { label: "CallNode" },
data: { label: "CallNode", badge: "HTTP" },
},
{
id: "n2",
Expand All @@ -60,7 +60,7 @@ const initialNodes: RF.Node[] = [
{
id: "n4",
type: GraphNodeType.Emit,
position: { x: -100, y: 300 },
position: { x: -150, y: 300 },
height: DEFAULT_NODE_SIZE.height,
width: DEFAULT_NODE_SIZE.width,
data: { label: "EmitNode" },
Expand All @@ -76,7 +76,7 @@ const initialNodes: RF.Node[] = [
{
id: "n6",
type: GraphNodeType.Fork,
position: { x: 300, y: 300 },
position: { x: 350, y: 300 },
height: DEFAULT_NODE_SIZE.height,
width: DEFAULT_NODE_SIZE.width,
data: { label: "Node 6" },
Expand All @@ -87,7 +87,7 @@ const initialNodes: RF.Node[] = [
position: { x: 100, y: 400 },
height: DEFAULT_NODE_SIZE.height,
width: DEFAULT_NODE_SIZE.width,
data: { label: "ListenNode" },
data: { label: "ListenNode", badge: "ALL" },
},
{
id: "n8",
Expand All @@ -103,7 +103,7 @@ const initialNodes: RF.Node[] = [
position: { x: 100, y: 600 },
height: DEFAULT_NODE_SIZE.height,
width: DEFAULT_NODE_SIZE.width,
data: { label: "RunNode" },
data: { label: "RunNode", badge: "myCustomType" },
},
{
id: "n10",
Expand Down Expand Up @@ -139,12 +139,11 @@ const initialEdges: RF.Edge[] = [
type: GraphEdgeType.Default,
data: {
wayPoints: [
{ x: 190, y: 60 },
{ x: 190, y: 70 },
{ x: 200, y: 70 },
{ x: 140, y: 70 },
{ x: 140, y: 85 },
{ x: 190, y: 85 },
{ x: 190, y: 95 },
{ x: 200, y: 85 },
{ x: 200, y: 95 },
],
},
},
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ import type React from "react";
import { GraphNodeType } from "@serverlessworkflow/sdk";
import * as RF from "@xyflow/react";
import { type LeafNodeType, taskNodeConfigMap } from "./taskNodeConfig";
import { Info } from "lucide-react";

// Node types must match sdk GraphNodeType enum
export const NodeTypes: RF.NodeTypes = {
Expand All @@ -31,13 +32,32 @@ export const NodeTypes: RF.NodeTypes = {
[GraphNodeType.Run]: RunNode,
[GraphNodeType.Set]: SetNode,
[GraphNodeType.Switch]: SwitchNode,
[GraphNodeType.TryCatch]: TryCatchNode,
[GraphNodeType.Try]: TryNode,
[GraphNodeType.Catch]: CatchNode,
[GraphNodeType.Wait]: WaitNode,
};

const KNOWN_BADGES = new Set([
"http",
"grpc",
"asyncapi",
"openapi",
"a2a",
"mcp",
"container",
"script",
"shell",
"workflow",
"all",
"any",
"one",
]);

export type BaseNodeData = {
// TODO: It is a placeholder, add properties to be consumed internally by node components
label: string;
badge?: string;
};

interface NodeContentProps {
Expand All @@ -47,6 +67,30 @@ interface NodeContentProps {
type: string;
}

interface BadgeProps {
badge: string;
testId: string;
}

function TaskNodeBadge({ badge, testId }: BadgeProps) {
const isUnknown = !KNOWN_BADGES.has(badge.toLowerCase());

if (isUnknown) {
/* TODO: instead of using the browser default to display tool tip like below, replace with tooltip component when we add it */
return (
<span title={badge} className="dec-task-node-badge-icon" data-testid={`${testId}-icon`}>
<Info size={18} />
Comment thread
lornakelly marked this conversation as resolved.
</span>
);
}

return (
<span className="dec-task-node-badge" data-testid={testId}>
{badge}
</span>
);
}

function TaskNodeContent({ id, data, selected, type }: NodeContentProps) {
const config = taskNodeConfigMap[type as LeafNodeType];
const Icon = config.icon;
Expand All @@ -63,6 +107,7 @@ function TaskNodeContent({ id, data, selected, type }: NodeContentProps) {
<span className="dec-task-node-name">{data.label}</span>
<span className="dec-task-node-type">{config.typeLabel}</span>
</div>
{data.badge && <TaskNodeBadge badge={data.badge} testId={`${type}-node-${id}-badge`} />}
</div>
<RF.Handle type="source" position={RF.Position.Bottom} />
</div>
Expand Down Expand Up @@ -93,77 +138,90 @@ function PlaceholderContent({ id, data, selected, type }: PlaceholderProps) {
);
}

/* call node */
/* call leaf node */
export type CallNodeType = RF.Node<BaseNodeData, typeof GraphNodeType.Call>;
export function CallNode({ id, data, selected, type }: RF.NodeProps<CallNodeType>) {
return <TaskNodeContent id={id} data={data} selected={selected} type={type} />;
}

/* do node */
/* do container node */
export type DoNodeType = RF.Node<BaseNodeData, typeof GraphNodeType.Do>;
export function DoNode({ id, data, selected, type }: RF.NodeProps<DoNodeType>) {
// TODO: This component is just a placeholder
return <PlaceholderContent id={id} data={data} selected={selected} type={type} />;
}

/* emit node */
/* emit leaf node */
export type EmitNodeType = RF.Node<BaseNodeData, typeof GraphNodeType.Emit>;
export function EmitNode({ id, data, selected, type }: RF.NodeProps<EmitNodeType>) {
return <TaskNodeContent id={id} data={data} selected={selected} type={type} />;
}

/* for node */
/* for container node */
export type ForNodeType = RF.Node<BaseNodeData, typeof GraphNodeType.For>;
export function ForNode({ id, data, selected, type }: RF.NodeProps<ForNodeType>) {
// TODO: This component is just a placeholder
return <PlaceholderContent id={id} data={data} selected={selected} type={type} />;
}

/* fork node */
/* fork container node */
export type ForkNodeType = RF.Node<BaseNodeData, typeof GraphNodeType.Fork>;
export function ForkNode({ id, data, selected, type }: RF.NodeProps<ForkNodeType>) {
// TODO: This component is just a placeholder
return <PlaceholderContent id={id} data={data} selected={selected} type={type} />;
}

/* listen node */
/* listen leaf node */
export type ListenNodeType = RF.Node<BaseNodeData, typeof GraphNodeType.Listen>;
export function ListenNode({ id, data, selected, type }: RF.NodeProps<ListenNodeType>) {
return <TaskNodeContent id={id} data={data} selected={selected} type={type} />;
}

/* raise node */
/* raise leaf node */
export type RaiseNodeType = RF.Node<BaseNodeData, typeof GraphNodeType.Raise>;
export function RaiseNode({ id, data, selected, type }: RF.NodeProps<RaiseNodeType>) {
return <TaskNodeContent id={id} data={data} selected={selected} type={type} />;
}

/* run node */
/* run leaf node */
export type RunNodeType = RF.Node<BaseNodeData, typeof GraphNodeType.Run>;
export function RunNode({ id, data, selected, type }: RF.NodeProps<RunNodeType>) {
return <TaskNodeContent id={id} data={data} selected={selected} type={type} />;
}

/* set node */
/* set leaf node */
export type SetNodeType = RF.Node<BaseNodeData, typeof GraphNodeType.Set>;
export function SetNode({ id, data, selected, type }: RF.NodeProps<SetNodeType>) {
return <TaskNodeContent id={id} data={data} selected={selected} type={type} />;
}

/* switch node */
/* switch leaf node */
export type SwitchNodeType = RF.Node<BaseNodeData, typeof GraphNodeType.Switch>;
export function SwitchNode({ id, data, selected, type }: RF.NodeProps<SwitchNodeType>) {
return <TaskNodeContent id={id} data={data} selected={selected} type={type} />;
}

/* try node */
/* try catch container node */
export type TryCatchNodeType = RF.Node<BaseNodeData, typeof GraphNodeType.TryCatch>;
export function TryCatchNode({ id, data, selected, type }: RF.NodeProps<TryCatchNodeType>) {
// TODO: This component is just a placeholder
return <PlaceholderContent id={id} data={data} selected={selected} type={type} />;
}

/* try container node */
export type TryNodeType = RF.Node<BaseNodeData, typeof GraphNodeType.Try>;
export function TryNode({ id, data, selected, type }: RF.NodeProps<TryNodeType>) {
// TODO: This component is just a placeholder
return <PlaceholderContent id={id} data={data} selected={selected} type={type} />;
}

/* wait node */
/* catch leaf node */
export type CatchNodeType = RF.Node<BaseNodeData, typeof GraphNodeType.Catch>;
export function CatchNode({ id, data, selected, type }: RF.NodeProps<CatchNodeType>) {
return <TaskNodeContent id={id} data={data} selected={selected} type={type} />;
}

/* wait leaf node */
export type WaitNodeType = RF.Node<BaseNodeData, typeof GraphNodeType.Wait>;
export function WaitNode({ id, data, selected, type }: RF.NodeProps<WaitNodeType>) {
return <TaskNodeContent id={id} data={data} selected={selected} type={type} />;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -23,12 +23,14 @@ import {
Megaphone,
PenLine,
Phone,
ShieldAlert,
Terminal,
} from "lucide-react";
import type { ComponentType } from "react";

export type LeafNodeType =
| typeof GraphNodeType.Call
| typeof GraphNodeType.Catch
| typeof GraphNodeType.Emit
| typeof GraphNodeType.Listen
| typeof GraphNodeType.Raise
Expand All @@ -49,6 +51,11 @@ export const taskNodeConfigMap: Record<LeafNodeType, TaskNodeConfig> = {
icon: Phone,
typeLabel: "CALL",
},
[GraphNodeType.Catch]: {
color: "#F97316",
icon: ShieldAlert,
typeLabel: "CATCH",
},
[GraphNodeType.Emit]: {
color: "#8B5CF6",
icon: Megaphone,
Expand Down
Loading
Loading