-
Notifications
You must be signed in to change notification settings - Fork 8
feat: Add badge to leaf nodes #138
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -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; | ||
| } | ||
| 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 |
|---|---|---|
|
|
@@ -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 = { | ||
|
|
@@ -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; | ||
|
|
@@ -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`}> | ||
|
lornakelly marked this conversation as resolved.
|
||
| <Info size={18} /> | ||
|
lornakelly marked this conversation as resolved.
lornakelly marked this conversation as resolved.
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) { | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Looking at the node components, we have the task base type already solved then it is converted back to a generic task type as prop to the TaskNodeComponent, finally the task type is solved again in the TaskNodeComponent to get a subtype if there is one. Maybe it could be possible to avoid the back and forth of task types. |
||
| 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 | ||
|
|
@@ -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> | ||
|
|
@@ -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 | ||
|
|
@@ -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 | ||
|
|
@@ -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 | ||
|
|
@@ -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} />; | ||
|
|
||
Uh oh!
There was an error while loading. Please reload this page.