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>
This commit is contained in:
Minamiyama
2025-08-05 15:08:23 +08:00
committed by GitHub
parent d080bea20b
commit 4934dbd0e6
10 changed files with 211 additions and 5 deletions

View File

@@ -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,
}}
/>
<EdgeLabelRenderer>

View File

@@ -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,
}
}

View File

@@ -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'],
},
)
}

View File

@@ -7,6 +7,7 @@ import { useTranslation } from 'react-i18next'
export enum TabType {
settings = 'settings',
lastRun = 'lastRun',
relations = 'relations',
}
type Props = {

View File

@@ -143,6 +143,7 @@ const BaseNode: FC<BaseNodeProps> = ({
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={{

View File

@@ -94,6 +94,7 @@ export type CommonNodeType<T = {}> = {
retry_config?: WorkflowRetryConfig
default_value?: DefaultValueForm[]
credential_id?: string
_dimmed?: boolean
} & T & Partial<Pick<ToolDefaultValue, 'provider_id' | 'provider_type' | 'provider_name' | 'tool_name'>>
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<T = {}> = ReactFlowNode<CommonNodeType<T>>