From 4934dbd0e62526288bc7059c7227c94b4534cad7 Mon Sep 17 00:00:00 2001 From: Minamiyama Date: Tue, 5 Aug 2025 15:08:23 +0800 Subject: [PATCH] feat(workflow): add relations panel to visualize dependencies (#21998) Co-authored-by: crazywoola <427733928@qq.com> Co-authored-by: crazywoola <100913391+crazywoola@users.noreply.github.com> --- web/app/components/workflow/custom-edge.tsx | 3 +- .../workflow/hooks/use-nodes-interactions.ts | 134 +++++++++++++++++- .../workflow/hooks/use-shortcuts.ts | 33 +++++ .../_base/components/workflow-panel/tab.tsx | 1 + .../components/workflow/nodes/_base/node.tsx | 1 + web/app/components/workflow/types.ts | 4 +- 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 +- 10 files changed, 211 insertions(+), 5 deletions(-) diff --git a/web/app/components/workflow/custom-edge.tsx b/web/app/components/workflow/custom-edge.tsx index 4467b0adb5..4874fc700b 100644 --- a/web/app/components/workflow/custom-edge.tsx +++ b/web/app/components/workflow/custom-edge.tsx @@ -134,7 +134,8 @@ const CustomEdge = ({ style={{ stroke, strokeWidth: 2, - opacity: data._waitingRun ? 0.7 : 1, + opacity: data._dimmed ? 0.3 : (data._waitingRun ? 0.7 : 1), + strokeDasharray: data._isTemp ? '8 8' : undefined, }} /> diff --git a/web/app/components/workflow/hooks/use-nodes-interactions.ts b/web/app/components/workflow/hooks/use-nodes-interactions.ts index b598951adb..fdfb25b04d 100644 --- a/web/app/components/workflow/hooks/use-nodes-interactions.ts +++ b/web/app/components/workflow/hooks/use-nodes-interactions.ts @@ -1,5 +1,5 @@ import type { MouseEvent } from 'react' -import { useCallback, useRef } from 'react' +import { useCallback, useRef, useState } from 'react' import { useTranslation } from 'react-i18next' import produce from 'immer' import type { @@ -61,6 +61,7 @@ import { } from './use-workflow' import { WorkflowHistoryEvent, useWorkflowHistory } from './use-workflow-history' import useInspectVarsCrud from './use-inspect-vars-crud' +import { getNodeUsedVars } from '../nodes/_base/components/variable/utils' export const useNodesInteractions = () => { const { t } = useTranslation() @@ -1530,6 +1531,135 @@ export const useNodesInteractions = () => { setNodes(nodes) }, [redo, store, workflowHistoryStore, getNodesReadOnly, getWorkflowReadOnly]) + const [isDimming, setIsDimming] = useState(false) + /** Add opacity-30 to all nodes except the nodeId */ + const dimOtherNodes = useCallback(() => { + if (isDimming) + return + const { getNodes, setNodes, edges, setEdges } = store.getState() + const nodes = getNodes() + + const selectedNode = nodes.find(n => n.data.selected) + if (!selectedNode) + return + + setIsDimming(true) + + // const workflowNodes = useStore(s => s.getNodes()) + const workflowNodes = nodes + + 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) + } + }) + + 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) + } + }) + + const dimNodes = [...dependencyNodes, ...dependentNodes, selectedNode] + + const newNodes = produce(nodes, (draft) => { + draft.forEach((n) => { + const dimNode = dimNodes.find(v => v.id === n.id) + if (!dimNode) + n.data._dimmed = true + }) + }) + + setNodes(newNodes) + + const tempEdges: Edge[] = [] + + dependencyNodes.forEach((n) => { + tempEdges.push({ + id: `tmp_${n.id}-source-${selectedNode.id}-target`, + type: CUSTOM_EDGE, + source: n.id, + sourceHandle: 'source_tmp', + target: selectedNode.id, + targetHandle: 'target_tmp', + animated: true, + data: { + sourceType: n.data.type, + targetType: selectedNode.data.type, + _isTemp: true, + _connectedNodeIsHovering: true, + }, + }) + }) + dependentNodes.forEach((n) => { + tempEdges.push({ + id: `tmp_${selectedNode.id}-source-${n.id}-target`, + type: CUSTOM_EDGE, + source: selectedNode.id, + sourceHandle: 'source_tmp', + target: n.id, + targetHandle: 'target_tmp', + animated: true, + data: { + sourceType: selectedNode.data.type, + targetType: n.data.type, + _isTemp: true, + _connectedNodeIsHovering: true, + }, + }) + }) + + const newEdges = produce(edges, (draft) => { + draft.forEach((e) => { + e.data._dimmed = true + }) + draft.push(...tempEdges) + }) + setEdges(newEdges) + }, [isDimming, store]) + + /** Restore all nodes to full opacity */ + const undimAllNodes = useCallback(() => { + const { getNodes, setNodes, edges, setEdges } = store.getState() + const nodes = getNodes() + setIsDimming(false) + + const newNodes = produce(nodes, (draft) => { + draft.forEach((n) => { + n.data._dimmed = false + }) + }) + + setNodes(newNodes) + + const newEdges = produce(edges.filter(e => !e.data._isTemp), (draft) => { + draft.forEach((e) => { + e.data._dimmed = false + }) + }) + setEdges(newEdges) + }, [store]) + return { handleNodeDragStart, handleNodeDrag, @@ -1554,5 +1684,7 @@ export const useNodesInteractions = () => { handleNodeDisconnect, handleHistoryBack, handleHistoryForward, + dimOtherNodes, + undimAllNodes, } } diff --git a/web/app/components/workflow/hooks/use-shortcuts.ts b/web/app/components/workflow/hooks/use-shortcuts.ts index 118ec94058..def4eef9ce 100644 --- a/web/app/components/workflow/hooks/use-shortcuts.ts +++ b/web/app/components/workflow/hooks/use-shortcuts.ts @@ -25,6 +25,8 @@ export const useShortcuts = (): void => { handleNodesDelete, handleHistoryBack, handleHistoryForward, + dimOtherNodes, + undimAllNodes, } = useNodesInteractions() const { handleStartWorkflowRun } = useWorkflowStartRun() const { shortcutsEnabled: workflowHistoryShortcutsEnabled } = useWorkflowHistoryStore() @@ -211,4 +213,35 @@ export const useShortcuts = (): void => { exactMatch: true, useCapture: true, }) + + // Shift ↓ + useKeyPress( + 'shift', + (e) => { + console.log('Shift down', e) + if (shouldHandleShortcut(e)) + dimOtherNodes() + }, + { + exactMatch: true, + useCapture: true, + events: ['keydown'], + }, + ) + + // Shift ↑ + useKeyPress( + (e) => { + return e.key === 'Shift' + }, + (e) => { + if (shouldHandleShortcut(e)) + undimAllNodes() + }, + { + exactMatch: true, + useCapture: true, + events: ['keyup'], + }, + ) } 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..08bbdf4068 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 = { diff --git a/web/app/components/workflow/nodes/_base/node.tsx b/web/app/components/workflow/nodes/_base/node.tsx index 68f2e3d572..c2600fd035 100644 --- a/web/app/components/workflow/nodes/_base/node.tsx +++ b/web/app/components/workflow/nodes/_base/node.tsx @@ -143,6 +143,7 @@ const BaseNode: FC = ({ showSelectedBorder ? 'border-components-option-card-option-selected-border' : 'border-transparent', !showSelectedBorder && data._inParallelHovering && 'border-workflow-block-border-highlight', data._waitingRun && 'opacity-70', + data._dimmed && 'opacity-30', )} ref={nodeRef} style={{ diff --git a/web/app/components/workflow/types.ts b/web/app/components/workflow/types.ts index 5840a04f26..61ebdb64a2 100644 --- a/web/app/components/workflow/types.ts +++ b/web/app/components/workflow/types.ts @@ -94,6 +94,7 @@ export type CommonNodeType = { retry_config?: WorkflowRetryConfig default_value?: DefaultValueForm[] credential_id?: string + _dimmed?: boolean } & T & Partial> export type CommonEdgeType = { @@ -109,7 +110,8 @@ export type CommonEdgeType = { isInLoop?: boolean loop_id?: string sourceType: BlockEnum - targetType: BlockEnum + targetType: BlockEnum, + _isTemp?: boolean, } export type Node = ReactFlowNode> diff --git a/web/i18n/en-US/workflow.ts b/web/i18n/en-US/workflow.ts index 9a8492f50e..10b74dadb3 100644 --- a/web/i18n/en-US/workflow.ts +++ b/web/i18n/en-US/workflow.ts @@ -943,6 +943,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', @@ -968,6 +969,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 59791c5c7e..a987efdfb1 100644 --- a/web/i18n/ja-JP/workflow.ts +++ b/web/i18n/ja-JP/workflow.ts @@ -968,6 +968,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 25f05ce8ba..dbc37a7b38 100644 --- a/web/i18n/zh-Hans/workflow.ts +++ b/web/i18n/zh-Hans/workflow.ts @@ -943,6 +943,7 @@ const translation = { debug: { settingsTab: '设置', lastRunTab: '上次运行', + relationsTab: '关系', noData: { description: '上次运行的结果将显示在这里', runThisNode: '运行此节点', @@ -968,6 +969,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 6a5e990909..ce10ad387f 100644 --- a/web/i18n/zh-Hant/workflow.ts +++ b/web/i18n/zh-Hant/workflow.ts @@ -941,6 +941,9 @@ const translation = { copyId: '複製ID', }, debug: { + settingsTab: '設定', + lastRunTab: '最後一次運行', + relationsTab: '關係', noData: { runThisNode: '運行此節點', description: '上次運行的結果將顯示在這裡', @@ -966,8 +969,14 @@ const translation = { emptyTip: '在畫布上逐步執行節點或逐步運行節點後,您可以在變數檢視中查看節點變數的當前值。', resetConversationVar: '將對話變數重置為默認值', }, - settingsTab: '設定', - lastRunTab: '最後一次運行', + relations: { + dependencies: '依賴', + dependents: '被依賴', + dependenciesDescription: '此節點所依賴的其他節點', + dependentsDescription: '依賴此節點的其他節點', + noDependencies: '無依賴', + noDependents: '無被依賴', + }, }, }