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={{
|
style={{
|
||||||
stroke,
|
stroke,
|
||||||
strokeWidth: 2,
|
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>
|
<EdgeLabelRenderer>
|
||||||
|
@@ -1,5 +1,5 @@
|
|||||||
import type { MouseEvent } from 'react'
|
import type { MouseEvent } from 'react'
|
||||||
import { useCallback, useRef } from 'react'
|
import { useCallback, useRef, useState } from 'react'
|
||||||
import { useTranslation } from 'react-i18next'
|
import { useTranslation } from 'react-i18next'
|
||||||
import produce from 'immer'
|
import produce from 'immer'
|
||||||
import type {
|
import type {
|
||||||
@@ -61,6 +61,7 @@ import {
|
|||||||
} from './use-workflow'
|
} from './use-workflow'
|
||||||
import { WorkflowHistoryEvent, useWorkflowHistory } from './use-workflow-history'
|
import { WorkflowHistoryEvent, useWorkflowHistory } from './use-workflow-history'
|
||||||
import useInspectVarsCrud from './use-inspect-vars-crud'
|
import useInspectVarsCrud from './use-inspect-vars-crud'
|
||||||
|
import { getNodeUsedVars } from '../nodes/_base/components/variable/utils'
|
||||||
|
|
||||||
export const useNodesInteractions = () => {
|
export const useNodesInteractions = () => {
|
||||||
const { t } = useTranslation()
|
const { t } = useTranslation()
|
||||||
@@ -1530,6 +1531,135 @@ export const useNodesInteractions = () => {
|
|||||||
setNodes(nodes)
|
setNodes(nodes)
|
||||||
}, [redo, store, workflowHistoryStore, getNodesReadOnly, getWorkflowReadOnly])
|
}, [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 {
|
return {
|
||||||
handleNodeDragStart,
|
handleNodeDragStart,
|
||||||
handleNodeDrag,
|
handleNodeDrag,
|
||||||
@@ -1554,5 +1684,7 @@ export const useNodesInteractions = () => {
|
|||||||
handleNodeDisconnect,
|
handleNodeDisconnect,
|
||||||
handleHistoryBack,
|
handleHistoryBack,
|
||||||
handleHistoryForward,
|
handleHistoryForward,
|
||||||
|
dimOtherNodes,
|
||||||
|
undimAllNodes,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@@ -25,6 +25,8 @@ export const useShortcuts = (): void => {
|
|||||||
handleNodesDelete,
|
handleNodesDelete,
|
||||||
handleHistoryBack,
|
handleHistoryBack,
|
||||||
handleHistoryForward,
|
handleHistoryForward,
|
||||||
|
dimOtherNodes,
|
||||||
|
undimAllNodes,
|
||||||
} = useNodesInteractions()
|
} = useNodesInteractions()
|
||||||
const { handleStartWorkflowRun } = useWorkflowStartRun()
|
const { handleStartWorkflowRun } = useWorkflowStartRun()
|
||||||
const { shortcutsEnabled: workflowHistoryShortcutsEnabled } = useWorkflowHistoryStore()
|
const { shortcutsEnabled: workflowHistoryShortcutsEnabled } = useWorkflowHistoryStore()
|
||||||
@@ -211,4 +213,35 @@ export const useShortcuts = (): void => {
|
|||||||
exactMatch: true,
|
exactMatch: true,
|
||||||
useCapture: 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 {
|
export enum TabType {
|
||||||
settings = 'settings',
|
settings = 'settings',
|
||||||
lastRun = 'lastRun',
|
lastRun = 'lastRun',
|
||||||
|
relations = 'relations',
|
||||||
}
|
}
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
|
@@ -143,6 +143,7 @@ const BaseNode: FC<BaseNodeProps> = ({
|
|||||||
showSelectedBorder ? 'border-components-option-card-option-selected-border' : 'border-transparent',
|
showSelectedBorder ? 'border-components-option-card-option-selected-border' : 'border-transparent',
|
||||||
!showSelectedBorder && data._inParallelHovering && 'border-workflow-block-border-highlight',
|
!showSelectedBorder && data._inParallelHovering && 'border-workflow-block-border-highlight',
|
||||||
data._waitingRun && 'opacity-70',
|
data._waitingRun && 'opacity-70',
|
||||||
|
data._dimmed && 'opacity-30',
|
||||||
)}
|
)}
|
||||||
ref={nodeRef}
|
ref={nodeRef}
|
||||||
style={{
|
style={{
|
||||||
|
@@ -94,6 +94,7 @@ export type CommonNodeType<T = {}> = {
|
|||||||
retry_config?: WorkflowRetryConfig
|
retry_config?: WorkflowRetryConfig
|
||||||
default_value?: DefaultValueForm[]
|
default_value?: DefaultValueForm[]
|
||||||
credential_id?: string
|
credential_id?: string
|
||||||
|
_dimmed?: boolean
|
||||||
} & T & Partial<Pick<ToolDefaultValue, 'provider_id' | 'provider_type' | 'provider_name' | 'tool_name'>>
|
} & T & Partial<Pick<ToolDefaultValue, 'provider_id' | 'provider_type' | 'provider_name' | 'tool_name'>>
|
||||||
|
|
||||||
export type CommonEdgeType = {
|
export type CommonEdgeType = {
|
||||||
@@ -109,7 +110,8 @@ export type CommonEdgeType = {
|
|||||||
isInLoop?: boolean
|
isInLoop?: boolean
|
||||||
loop_id?: string
|
loop_id?: string
|
||||||
sourceType: BlockEnum
|
sourceType: BlockEnum
|
||||||
targetType: BlockEnum
|
targetType: BlockEnum,
|
||||||
|
_isTemp?: boolean,
|
||||||
}
|
}
|
||||||
|
|
||||||
export type Node<T = {}> = ReactFlowNode<CommonNodeType<T>>
|
export type Node<T = {}> = ReactFlowNode<CommonNodeType<T>>
|
||||||
|
@@ -943,6 +943,7 @@ const translation = {
|
|||||||
debug: {
|
debug: {
|
||||||
settingsTab: 'Settings',
|
settingsTab: 'Settings',
|
||||||
lastRunTab: 'Last Run',
|
lastRunTab: 'Last Run',
|
||||||
|
relationsTab: 'Relations',
|
||||||
noData: {
|
noData: {
|
||||||
description: 'The results of the last run will be displayed here',
|
description: 'The results of the last run will be displayed here',
|
||||||
runThisNode: 'Run this node',
|
runThisNode: 'Run this node',
|
||||||
@@ -968,6 +969,14 @@ const translation = {
|
|||||||
chatNode: 'Conversation',
|
chatNode: 'Conversation',
|
||||||
systemNode: 'System',
|
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',
|
||||||
|
},
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@@ -968,6 +968,15 @@ const translation = {
|
|||||||
},
|
},
|
||||||
settingsTab: '設定',
|
settingsTab: '設定',
|
||||||
lastRunTab: '最後の実行',
|
lastRunTab: '最後の実行',
|
||||||
|
relationsTab: '関係',
|
||||||
|
relations: {
|
||||||
|
dependencies: '依存元',
|
||||||
|
dependents: '依存先',
|
||||||
|
dependenciesDescription: 'このノードが依存している他のノード',
|
||||||
|
dependentsDescription: 'このノードに依存している他のノード',
|
||||||
|
noDependencies: '依存元なし',
|
||||||
|
noDependents: '依存先なし',
|
||||||
|
},
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@@ -943,6 +943,7 @@ const translation = {
|
|||||||
debug: {
|
debug: {
|
||||||
settingsTab: '设置',
|
settingsTab: '设置',
|
||||||
lastRunTab: '上次运行',
|
lastRunTab: '上次运行',
|
||||||
|
relationsTab: '关系',
|
||||||
noData: {
|
noData: {
|
||||||
description: '上次运行的结果将显示在这里',
|
description: '上次运行的结果将显示在这里',
|
||||||
runThisNode: '运行此节点',
|
runThisNode: '运行此节点',
|
||||||
@@ -968,6 +969,14 @@ const translation = {
|
|||||||
chatNode: '会话变量',
|
chatNode: '会话变量',
|
||||||
systemNode: '系统变量',
|
systemNode: '系统变量',
|
||||||
},
|
},
|
||||||
|
relations: {
|
||||||
|
dependencies: '依赖',
|
||||||
|
dependents: '被依赖',
|
||||||
|
dependenciesDescription: '本节点依赖的其他节点',
|
||||||
|
dependentsDescription: '依赖于本节点的其他节点',
|
||||||
|
noDependencies: '无依赖',
|
||||||
|
noDependents: '无被依赖',
|
||||||
|
},
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@@ -941,6 +941,9 @@ const translation = {
|
|||||||
copyId: '複製ID',
|
copyId: '複製ID',
|
||||||
},
|
},
|
||||||
debug: {
|
debug: {
|
||||||
|
settingsTab: '設定',
|
||||||
|
lastRunTab: '最後一次運行',
|
||||||
|
relationsTab: '關係',
|
||||||
noData: {
|
noData: {
|
||||||
runThisNode: '運行此節點',
|
runThisNode: '運行此節點',
|
||||||
description: '上次運行的結果將顯示在這裡',
|
description: '上次運行的結果將顯示在這裡',
|
||||||
@@ -966,8 +969,14 @@ const translation = {
|
|||||||
emptyTip: '在畫布上逐步執行節點或逐步運行節點後,您可以在變數檢視中查看節點變數的當前值。',
|
emptyTip: '在畫布上逐步執行節點或逐步運行節點後,您可以在變數檢視中查看節點變數的當前值。',
|
||||||
resetConversationVar: '將對話變數重置為默認值',
|
resetConversationVar: '將對話變數重置為默認值',
|
||||||
},
|
},
|
||||||
settingsTab: '設定',
|
relations: {
|
||||||
lastRunTab: '最後一次運行',
|
dependencies: '依賴',
|
||||||
|
dependents: '被依賴',
|
||||||
|
dependenciesDescription: '此節點所依賴的其他節點',
|
||||||
|
dependentsDescription: '依賴此節點的其他節點',
|
||||||
|
noDependencies: '無依賴',
|
||||||
|
noDependents: '無被依賴',
|
||||||
|
},
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Reference in New Issue
Block a user