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:
@@ -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>
|
||||
|
@@ -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,
|
||||
}
|
||||
}
|
||||
|
@@ -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'],
|
||||
},
|
||||
)
|
||||
}
|
||||
|
@@ -7,6 +7,7 @@ import { useTranslation } from 'react-i18next'
|
||||
export enum TabType {
|
||||
settings = 'settings',
|
||||
lastRun = 'lastRun',
|
||||
relations = 'relations',
|
||||
}
|
||||
|
||||
type Props = {
|
||||
|
@@ -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={{
|
||||
|
@@ -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>>
|
||||
|
Reference in New Issue
Block a user