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 @@ -16,3 +16,4 @@

export * from "./workflowSdk";
export * from "./graph";
export * from "./taskSubType";
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
/*
* 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" && !Array.isArray(run)) {
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" && !Array.isArray(listen)) {
const firstKey = Object.keys(listen)[0];
return firstKey ?? undefined;
}
Comment thread
lornakelly marked this conversation as resolved.
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 /*TODO: for now truncate text revisit when layout is in and working on styling tweaks */
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 @@ -19,7 +19,7 @@ import { ReactFlowGraph } from "./diagramBuilder";
// Defaults
export const DEFAULT_NODE_SIZE = {
height: 60,
width: 180,
width: 200,
};

export type Point = {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,8 @@ import type React from "react";
import { GraphNodeType, type Specification } from "@serverlessworkflow/sdk";
import * as RF from "@xyflow/react";
import { type LeafNodeType, taskNodeConfigMap } from "./taskNodeConfig";
import { Info } from "lucide-react";
import { getTaskSubType } from "../../core";

// Node types must match sdk GraphNodeType enum
export const ReactFlowNodeTypes: RF.NodeTypes = {
Expand All @@ -33,10 +35,28 @@ export const ReactFlowNodeTypes: 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<T = Specification.Task | void> = {
label: string;
task?: T;
Expand All @@ -49,8 +69,35 @@ 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`}>
Comment thread
lornakelly marked this conversation as resolved.
<Info size={18} />
Comment thread
lornakelly marked this conversation as resolved.
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 badge = data.task
? getTaskSubType(type as GraphNodeType, data.task as Specification.Task)
: undefined;
const Icon = config.icon;
return (
<div
Expand All @@ -65,6 +112,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>
{badge && <TaskNodeBadge badge={badge} testId={`${type}-node-${id}-badge`} />}
</div>
<RF.Handle type="source" position={RF.Position.Bottom} />
</div>
Expand Down Expand Up @@ -109,40 +157,40 @@ export function EndNode({ id, data, selected, type }: RF.NodeProps<EndNodeType>)
return <PlaceholderContent id={id} data={data} selected={selected} type={type} />;
}

/* call node */
/* call leaf node */
export type CallNodeType = RF.Node<BaseNodeData<Specification.CallTask>, 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<Specification.DoTask>, 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<Specification.EmitTask>, 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<Specification.ForTask>, 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<Specification.ForkTask>, 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<Specification.ListenTask>,
typeof GraphNodeType.Listen
Expand All @@ -151,7 +199,7 @@ export function ListenNode({ id, data, selected, type }: RF.NodeProps<ListenNode
return <TaskNodeContent id={id} data={data} selected={selected} type={type} />;
}

/* raise node */
/* raise leaf node */
export type RaiseNodeType = RF.Node<
BaseNodeData<Specification.RaiseTask>,
typeof GraphNodeType.Raise
Expand All @@ -160,19 +208,19 @@ export function RaiseNode({ id, data, selected, type }: RF.NodeProps<RaiseNodeTy
return <TaskNodeContent id={id} data={data} selected={selected} type={type} />;
}

/* run node */
/* run leaf node */
export type RunNodeType = RF.Node<BaseNodeData<Specification.RunTask>, 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<Specification.SetTask>, 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<Specification.SwitchTask>,
typeof GraphNodeType.Switch
Expand All @@ -181,14 +229,27 @@ export function SwitchNode({ id, data, selected, type }: RF.NodeProps<SwitchNode
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<Specification.TryTask>, 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<Specification.WaitTask>, 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