From 12e1b82328cb018878608d4412af4987006939eb Mon Sep 17 00:00:00 2001 From: Minamiyama Date: Mon, 7 Jul 2025 18:28:35 +0800 Subject: [PATCH 1/2] feat(workflow): add node relations panel to display dependencies Introduce a new relations tab in workflow panel to visualize node dependencies and dependents. Includes: - New RelationType enum for dependency types - Internationalized UI text for multiple languages - Components for displaying relations with arrows and empty states - Logic to determine and display dependency relationships --- .../_base/components/workflow-panel/index.tsx | 26 ++++ .../workflow-panel/relations/container.tsx | 35 +++++ .../workflow-panel/relations/empty.tsx | 27 ++++ .../workflow-panel/relations/index.tsx | 121 ++++++++++++++++++ .../workflow-panel/relations/item.tsx | 59 +++++++++ .../workflow-panel/relations/line.tsx | 92 +++++++++++++ .../workflow-panel/relations/types.ts | 4 + .../_base/components/workflow-panel/tab.tsx | 2 + web/i18n/en-US/workflow.ts | 9 ++ web/i18n/ja-JP/workflow.ts | 9 ++ web/i18n/zh-Hans/workflow.ts | 9 ++ web/i18n/zh-Hant/workflow.ts | 13 +- 12 files changed, 404 insertions(+), 2 deletions(-) create mode 100644 web/app/components/workflow/nodes/_base/components/workflow-panel/relations/container.tsx create mode 100644 web/app/components/workflow/nodes/_base/components/workflow-panel/relations/empty.tsx create mode 100644 web/app/components/workflow/nodes/_base/components/workflow-panel/relations/index.tsx create mode 100644 web/app/components/workflow/nodes/_base/components/workflow-panel/relations/item.tsx create mode 100644 web/app/components/workflow/nodes/_base/components/workflow-panel/relations/line.tsx create mode 100644 web/app/components/workflow/nodes/_base/components/workflow-panel/relations/types.ts diff --git a/web/app/components/workflow/nodes/_base/components/workflow-panel/index.tsx b/web/app/components/workflow/nodes/_base/components/workflow-panel/index.tsx index 164369e64c..e1b7364b07 100644 --- a/web/app/components/workflow/nodes/_base/components/workflow-panel/index.tsx +++ b/web/app/components/workflow/nodes/_base/components/workflow-panel/index.tsx @@ -59,6 +59,8 @@ import { useLogs } from '@/app/components/workflow/run/hooks' import PanelWrap from '../before-run-form/panel-wrap' import SpecialResultPanel from '@/app/components/workflow/run/special-result-panel' import { Stop } from '@/app/components/base/icons/src/vender/line/mediaAndDevices' +import Relations from './relations' +import { RelationType } from './relations/types' type BasePanelProps = { children: ReactNode @@ -420,6 +422,30 @@ const BasePanel: FC = ({ {...passedLogParams} /> )} + + {tabType === TabType.relations && ( + <> +
+
+ {t('workflow.debug.relations.dependencies').toLocaleUpperCase()} +
+
+ {t('workflow.debug.relations.dependenciesDescription')} +
+ +
+ +
+
+ {t('workflow.debug.relations.dependents').toLocaleUpperCase()} +
+
+ {t('workflow.debug.relations.dependentsDescription')} +
+ +
+ + )} ) diff --git a/web/app/components/workflow/nodes/_base/components/workflow-panel/relations/container.tsx b/web/app/components/workflow/nodes/_base/components/workflow-panel/relations/container.tsx new file mode 100644 index 0000000000..52802c45e3 --- /dev/null +++ b/web/app/components/workflow/nodes/_base/components/workflow-panel/relations/container.tsx @@ -0,0 +1,35 @@ +import Empty from './empty' +import Item from './item' +import type { Node } from '@/app/components/workflow/types' +import cn from '@/utils/classnames' +import { RelationType } from './types' +import { useTranslation } from 'react-i18next' +type ContainerProps = { + nextNode?: Node + relationType: RelationType +} + +const Container = ({ + nextNode, + relationType, +}: ContainerProps) => { + const { t } = useTranslation() + return ( +
+ {nextNode && ( + + )} + {!nextNode && ( + + )} +
+ ) +} + +export default Container diff --git a/web/app/components/workflow/nodes/_base/components/workflow-panel/relations/empty.tsx b/web/app/components/workflow/nodes/_base/components/workflow-panel/relations/empty.tsx new file mode 100644 index 0000000000..c0ab918c57 --- /dev/null +++ b/web/app/components/workflow/nodes/_base/components/workflow-panel/relations/empty.tsx @@ -0,0 +1,27 @@ +import { memo } from 'react' + +type EmptyProps = { + display: string +} + +const Empty = ({ + display = '', +}: EmptyProps) => { + return ( + <> +
+
+ {display} +
+
+ + ) +} + +export default memo(Empty) diff --git a/web/app/components/workflow/nodes/_base/components/workflow-panel/relations/index.tsx b/web/app/components/workflow/nodes/_base/components/workflow-panel/relations/index.tsx new file mode 100644 index 0000000000..656c88f4dd --- /dev/null +++ b/web/app/components/workflow/nodes/_base/components/workflow-panel/relations/index.tsx @@ -0,0 +1,121 @@ +import { memo } from 'react' +import { isEqual } from 'lodash-es' +import { + getOutgoers, + useStore, +} from 'reactflow' +import { useToolIcon } from '@/app/components/workflow/hooks' +import BlockIcon from '@/app/components/workflow/block-icon' +import type { + Node, +} from '@/app/components/workflow/types' +import Line from './line' +import Container from './container' +import { getNodeUsedVars } from '../../variable/utils' +import { RelationType } from './types' + +type RelationsProps = { + selectedNode: Node + relationType: RelationType +} +const Relations = ({ + selectedNode, + relationType, +}: RelationsProps) => { + const data = selectedNode.data + const toolIcon = useToolIcon(data) + const edges = useStore(s => s.edges.map(edge => ({ + id: edge.id, + source: edge.source, + sourceHandle: edge.sourceHandle, + target: edge.target, + targetHandle: edge.targetHandle, + })), isEqual) + const nodes = useStore(s => s.getNodes().map(node => ({ + id: node.id, + data: node.data, + })), isEqual) + const workflowNodes = useStore(s => s.getNodes()) + + const list: Node[] = [] + + if (relationType === RelationType.dependencies) { + const usedVars = getNodeUsedVars(selectedNode) + const dependencyNodes: Node[] = [] + usedVars.forEach((valueSelector) => { + const node = workflowNodes.find(node => node.id === valueSelector?.[0]) + if (node) { + if (!dependencyNodes.includes(node)) + dependencyNodes.push(node) + } + }) + list.push(...dependencyNodes) + } + else { + const outgoers = getOutgoers(selectedNode as Node, nodes as Node[], edges) + for (let currIdx = 0; currIdx < outgoers.length; currIdx++) { + const node = outgoers[currIdx] + const outgoersForNode = getOutgoers(node, nodes as Node[], edges) + outgoersForNode.forEach((item) => { + const existed = outgoers.some(v => v.id === item.id) + if (!existed) + outgoers.push(item) + }) + } + + const dependentNodes: Node[] = [] + outgoers.forEach((node) => { + const usedVars = getNodeUsedVars(node) + const used = usedVars.some(v => v?.[0] === selectedNode.id) + if (used) { + const existed = dependentNodes.some(v => v.id === node.id) + if (!existed) + dependentNodes.push(node) + } + }) + list.push(...dependentNodes) + } + + const getThisNode = () => { + return ( +
+ +
+ ) + } + + const getLinkedNodes = () => { + return ( +
+ {list.length > 0 ? ( + list.map((item, index) => { + return ( + + ) + }) + ) : ( + + )} +
+ ) + } + + return ( +
+ {getThisNode()} + + {getLinkedNodes()} +
+ ) +} + +export default memo(Relations) diff --git a/web/app/components/workflow/nodes/_base/components/workflow-panel/relations/item.tsx b/web/app/components/workflow/nodes/_base/components/workflow-panel/relations/item.tsx new file mode 100644 index 0000000000..253d6480e9 --- /dev/null +++ b/web/app/components/workflow/nodes/_base/components/workflow-panel/relations/item.tsx @@ -0,0 +1,59 @@ +import { memo } from 'react' +import { useTranslation } from 'react-i18next' +import type { + CommonNodeType, +} from '@/app/components/workflow/types' +import BlockIcon from '@/app/components/workflow/block-icon' +import { + useNodesInteractions, + useNodesReadOnly, + useToolIcon, +} from '@/app/components/workflow/hooks' +import Button from '@/app/components/base/button' + +type ItemProps = { + nodeId: string + data: CommonNodeType +} +const Item = ({ + nodeId, + data, +}: ItemProps) => { + const { t } = useTranslation() + const { nodesReadOnly } = useNodesReadOnly() + const { handleNodeSelect } = useNodesInteractions() + const toolIcon = useToolIcon(data) + + return ( +
+ +
+ {data.title} +
+ { + !nodesReadOnly && ( + <> + + + ) + } +
+ ) +} + +export default memo(Item) diff --git a/web/app/components/workflow/nodes/_base/components/workflow-panel/relations/line.tsx b/web/app/components/workflow/nodes/_base/components/workflow-panel/relations/line.tsx new file mode 100644 index 0000000000..1f2f5ff274 --- /dev/null +++ b/web/app/components/workflow/nodes/_base/components/workflow-panel/relations/line.tsx @@ -0,0 +1,92 @@ +import { memo } from 'react' +import { RelationType } from './types' + +type LineProps = { + rowCount: number + relationType: RelationType +} +const Line = ({ + rowCount = 1, + relationType, +}: LineProps) => { + const list: number[] = [] + const listH = 40 + const ySpacing = 8 + + const svgHeight = listH * rowCount + ySpacing * (rowCount - 1) + + const lineWidth = 48 + const arrowStrokeWidth = 1 + const arrowLineLength = 6 + + return ( + + { + Array.from({ length: rowCount }).map((_, index) => { + const space = index * listH + index * ySpacing + 16 + return ( + + { + index === 0 && ( + <> + + + + + ) + } + { + index > 0 && ( + <> + + {relationType === RelationType.dependents && ( + + )} + + ) + } + + + ) + }) + } + + ) +} + +export default memo(Line) diff --git a/web/app/components/workflow/nodes/_base/components/workflow-panel/relations/types.ts b/web/app/components/workflow/nodes/_base/components/workflow-panel/relations/types.ts new file mode 100644 index 0000000000..274de73ca5 --- /dev/null +++ b/web/app/components/workflow/nodes/_base/components/workflow-panel/relations/types.ts @@ -0,0 +1,4 @@ +export enum RelationType { + dependencies, + dependents, +} diff --git a/web/app/components/workflow/nodes/_base/components/workflow-panel/tab.tsx b/web/app/components/workflow/nodes/_base/components/workflow-panel/tab.tsx index 09d7ed266d..913ecdc758 100644 --- a/web/app/components/workflow/nodes/_base/components/workflow-panel/tab.tsx +++ b/web/app/components/workflow/nodes/_base/components/workflow-panel/tab.tsx @@ -7,6 +7,7 @@ import { useTranslation } from 'react-i18next' export enum TabType { settings = 'settings', lastRun = 'lastRun', + relations = 'relations', } type Props = { @@ -24,6 +25,7 @@ const Tab: FC = ({ items={[ { id: TabType.settings, name: t('workflow.debug.settingsTab').toLocaleUpperCase() }, { id: TabType.lastRun, name: t('workflow.debug.lastRunTab').toLocaleUpperCase() }, + { id: TabType.relations, name: t('workflow.debug.relationsTab').toLocaleUpperCase() }, ]} itemClassName='ml-0' value={value} diff --git a/web/i18n/en-US/workflow.ts b/web/i18n/en-US/workflow.ts index df0bb904fd..980d6e0de3 100644 --- a/web/i18n/en-US/workflow.ts +++ b/web/i18n/en-US/workflow.ts @@ -919,6 +919,7 @@ const translation = { debug: { settingsTab: 'Settings', lastRunTab: 'Last Run', + relationsTab: 'Relations', noData: { description: 'The results of the last run will be displayed here', runThisNode: 'Run this node', @@ -944,6 +945,14 @@ const translation = { chatNode: 'Conversation', systemNode: 'System', }, + relations: { + dependencies: 'Dependencies', + dependents: 'Dependents', + dependenciesDescription: 'Nodes that this node relies on', + dependentsDescription: 'Nodes that rely on this node', + noDependencies: 'No dependencies', + noDependents: 'No dependents', + }, }, } diff --git a/web/i18n/ja-JP/workflow.ts b/web/i18n/ja-JP/workflow.ts index 3c959669bf..bceec4e860 100644 --- a/web/i18n/ja-JP/workflow.ts +++ b/web/i18n/ja-JP/workflow.ts @@ -947,6 +947,15 @@ const translation = { }, settingsTab: '設定', lastRunTab: '最後の実行', + relationsTab: '関係', + relations: { + dependencies: '依存元', + dependents: '依存先', + dependenciesDescription: 'このノードが依存している他のノード', + dependentsDescription: 'このノードに依存している他のノード', + noDependencies: '依存元なし', + noDependents: '依存先なし', + }, }, } diff --git a/web/i18n/zh-Hans/workflow.ts b/web/i18n/zh-Hans/workflow.ts index aa9dcae261..96d56ae462 100644 --- a/web/i18n/zh-Hans/workflow.ts +++ b/web/i18n/zh-Hans/workflow.ts @@ -920,6 +920,7 @@ const translation = { debug: { settingsTab: '设置', lastRunTab: '上次运行', + relationsTab: '关系', noData: { description: '上次运行的结果将显示在这里', runThisNode: '运行此节点', @@ -945,6 +946,14 @@ const translation = { chatNode: '会话变量', systemNode: '系统变量', }, + relations: { + dependencies: '依赖', + dependents: '被依赖', + dependenciesDescription: '本节点依赖的其他节点', + dependentsDescription: '依赖于本节点的其他节点', + noDependencies: '无依赖', + noDependents: '无被依赖', + }, }, } diff --git a/web/i18n/zh-Hant/workflow.ts b/web/i18n/zh-Hant/workflow.ts index 8262e60351..417bc85dfe 100644 --- a/web/i18n/zh-Hant/workflow.ts +++ b/web/i18n/zh-Hant/workflow.ts @@ -921,6 +921,9 @@ const translation = { defaultName: '未命名版本', }, debug: { + settingsTab: '設定', + lastRunTab: '最後一次運行', + relationsTab: '關係', noData: { runThisNode: '運行此節點', description: '上次運行的結果將顯示在這裡', @@ -946,8 +949,14 @@ const translation = { emptyTip: '在畫布上逐步執行節點或逐步運行節點後,您可以在變數檢視中查看節點變數的當前值。', resetConversationVar: '將對話變數重置為默認值', }, - settingsTab: '設定', - lastRunTab: '最後一次運行', + relations: { + dependencies: '依賴', + dependents: '被依賴', + dependenciesDescription: '此節點所依賴的其他節點', + dependentsDescription: '依賴此節點的其他節點', + noDependencies: '無依賴', + noDependents: '無被依賴', + }, }, } From b1248792738f07d2ea2f185e0e2beb53fa7546f8 Mon Sep 17 00:00:00 2001 From: Mminamiyama Date: Sat, 12 Jul 2025 12:10:04 +0800 Subject: [PATCH 2/2] fix(workflow-panel): pass relationType prop to all Container instances Ensure consistent behavior by passing relationType prop to Container components in both conditional rendering cases --- .../nodes/_base/components/workflow-panel/relations/index.tsx | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/web/app/components/workflow/nodes/_base/components/workflow-panel/relations/index.tsx b/web/app/components/workflow/nodes/_base/components/workflow-panel/relations/index.tsx index 656c88f4dd..1edf8b6b0d 100644 --- a/web/app/components/workflow/nodes/_base/components/workflow-panel/relations/index.tsx +++ b/web/app/components/workflow/nodes/_base/components/workflow-panel/relations/index.tsx @@ -96,11 +96,12 @@ const Relations = ({ ) }) ) : ( - + )} )