mirror of
https://github.com/langgenius/dify.git
synced 2026-05-02 08:28:03 +08:00
feat(web): unify node selector availability rules
This commit is contained in:
142
web/app/components/workflow/constants/node-availability.spec.ts
Normal file
142
web/app/components/workflow/constants/node-availability.spec.ts
Normal file
@ -0,0 +1,142 @@
|
||||
import { describe, expect, it } from 'vitest'
|
||||
import { BlockEnum } from '@/app/components/workflow/types'
|
||||
import {
|
||||
buildNodeSelectorAvailabilityContext,
|
||||
filterNodesForSelector,
|
||||
isNodeAvailableInSelector,
|
||||
NodeSelectorSandboxMode,
|
||||
NodeSelectorScene,
|
||||
} from './node-availability'
|
||||
|
||||
type MockNode = {
|
||||
metaData: {
|
||||
type: BlockEnum
|
||||
}
|
||||
}
|
||||
|
||||
describe('node-availability', () => {
|
||||
it('should hide command and file-upload when sandbox is disabled', () => {
|
||||
const mockNodes: MockNode[] = [
|
||||
{ metaData: { type: BlockEnum.Start } },
|
||||
{ metaData: { type: BlockEnum.Command } },
|
||||
{ metaData: { type: BlockEnum.FileUpload } },
|
||||
]
|
||||
|
||||
const result = filterNodesForSelector(mockNodes, {
|
||||
scene: NodeSelectorScene.Workflow,
|
||||
sandboxMode: NodeSelectorSandboxMode.Disabled,
|
||||
})
|
||||
const nodeTypes = result.map(node => node.metaData.type)
|
||||
|
||||
expect(nodeTypes).toEqual([BlockEnum.Start])
|
||||
})
|
||||
|
||||
it('should keep command and file-upload when sandbox is enabled', () => {
|
||||
const mockNodes: MockNode[] = [
|
||||
{ metaData: { type: BlockEnum.Start } },
|
||||
{ metaData: { type: BlockEnum.Command } },
|
||||
{ metaData: { type: BlockEnum.FileUpload } },
|
||||
]
|
||||
|
||||
const result = filterNodesForSelector(mockNodes, {
|
||||
scene: NodeSelectorScene.Workflow,
|
||||
sandboxMode: NodeSelectorSandboxMode.Enabled,
|
||||
})
|
||||
const nodeTypes = result.map(node => node.metaData.type)
|
||||
|
||||
expect(nodeTypes).toEqual([BlockEnum.Start, BlockEnum.Command, BlockEnum.FileUpload])
|
||||
})
|
||||
|
||||
it('should return original reference when no filtering is needed', () => {
|
||||
const mockNodes: MockNode[] = [
|
||||
{ metaData: { type: BlockEnum.Start } },
|
||||
{ metaData: { type: BlockEnum.End } },
|
||||
]
|
||||
|
||||
const result = filterNodesForSelector(mockNodes, {
|
||||
scene: NodeSelectorScene.Workflow,
|
||||
sandboxMode: NodeSelectorSandboxMode.Disabled,
|
||||
})
|
||||
|
||||
expect(result).toBe(mockNodes)
|
||||
})
|
||||
|
||||
it('should mark command and file-upload as sandbox-only', () => {
|
||||
expect(isNodeAvailableInSelector(BlockEnum.Command, {
|
||||
scene: NodeSelectorScene.Workflow,
|
||||
sandboxMode: NodeSelectorSandboxMode.Disabled,
|
||||
})).toBe(false)
|
||||
expect(isNodeAvailableInSelector(BlockEnum.FileUpload, {
|
||||
scene: NodeSelectorScene.Workflow,
|
||||
sandboxMode: NodeSelectorSandboxMode.Disabled,
|
||||
})).toBe(false)
|
||||
expect(isNodeAvailableInSelector(BlockEnum.Start, {
|
||||
scene: NodeSelectorScene.Workflow,
|
||||
sandboxMode: NodeSelectorSandboxMode.Disabled,
|
||||
})).toBe(true)
|
||||
})
|
||||
|
||||
it('should hide agent when sandbox is enabled', () => {
|
||||
expect(isNodeAvailableInSelector(BlockEnum.Agent, {
|
||||
scene: NodeSelectorScene.Workflow,
|
||||
sandboxMode: NodeSelectorSandboxMode.Enabled,
|
||||
})).toBe(false)
|
||||
expect(isNodeAvailableInSelector(BlockEnum.Agent, {
|
||||
scene: NodeSelectorScene.Workflow,
|
||||
sandboxMode: NodeSelectorSandboxMode.Disabled,
|
||||
})).toBe(true)
|
||||
})
|
||||
|
||||
it('should hide human-input in rag pipeline flow', () => {
|
||||
expect(isNodeAvailableInSelector(BlockEnum.HumanInput, {
|
||||
scene: NodeSelectorScene.RagPipeline,
|
||||
sandboxMode: NodeSelectorSandboxMode.Enabled,
|
||||
})).toBe(false)
|
||||
expect(isNodeAvailableInSelector(BlockEnum.HumanInput, {
|
||||
scene: NodeSelectorScene.RagPipeline,
|
||||
sandboxMode: NodeSelectorSandboxMode.Disabled,
|
||||
})).toBe(false)
|
||||
expect(isNodeAvailableInSelector(BlockEnum.HumanInput, {
|
||||
scene: NodeSelectorScene.Workflow,
|
||||
sandboxMode: NodeSelectorSandboxMode.Disabled,
|
||||
})).toBe(true)
|
||||
})
|
||||
|
||||
it('should build unsupported sandbox mode for scenes that do not support sandbox', () => {
|
||||
const context = buildNodeSelectorAvailabilityContext({
|
||||
scene: NodeSelectorScene.RagPipeline,
|
||||
isSandboxRuntime: true,
|
||||
isSandboxFeatureEnabled: true,
|
||||
})
|
||||
|
||||
expect(context.scene).toBe(NodeSelectorScene.RagPipeline)
|
||||
expect(context.sandboxMode).toBe(NodeSelectorSandboxMode.Unsupported)
|
||||
})
|
||||
|
||||
it('should allow explicit scene sandbox support override', () => {
|
||||
const context = buildNodeSelectorAvailabilityContext({
|
||||
scene: NodeSelectorScene.RagPipeline,
|
||||
supportsSandbox: true,
|
||||
isSandboxRuntime: true,
|
||||
isSandboxFeatureEnabled: false,
|
||||
})
|
||||
|
||||
expect(context.sandboxMode).toBe(NodeSelectorSandboxMode.Enabled)
|
||||
})
|
||||
|
||||
it('should build enabled sandbox mode when runtime or feature enables sandbox', () => {
|
||||
const contextByRuntime = buildNodeSelectorAvailabilityContext({
|
||||
scene: NodeSelectorScene.Workflow,
|
||||
isSandboxRuntime: true,
|
||||
isSandboxFeatureEnabled: false,
|
||||
})
|
||||
const contextByFeature = buildNodeSelectorAvailabilityContext({
|
||||
scene: NodeSelectorScene.Workflow,
|
||||
isSandboxRuntime: false,
|
||||
isSandboxFeatureEnabled: true,
|
||||
})
|
||||
|
||||
expect(contextByRuntime.sandboxMode).toBe(NodeSelectorSandboxMode.Enabled)
|
||||
expect(contextByFeature.sandboxMode).toBe(NodeSelectorSandboxMode.Enabled)
|
||||
})
|
||||
})
|
||||
115
web/app/components/workflow/constants/node-availability.ts
Normal file
115
web/app/components/workflow/constants/node-availability.ts
Normal file
@ -0,0 +1,115 @@
|
||||
import { BlockEnum } from '@/app/components/workflow/types'
|
||||
|
||||
export enum NodeSelectorScene {
|
||||
Workflow = 'workflow',
|
||||
Chatflow = 'chatflow',
|
||||
RagPipeline = 'rag-pipeline',
|
||||
Subgraph = 'subgraph',
|
||||
}
|
||||
|
||||
export enum NodeSelectorSandboxMode {
|
||||
Enabled = 'enabled',
|
||||
Disabled = 'disabled',
|
||||
Unsupported = 'unsupported',
|
||||
}
|
||||
|
||||
export const NODE_SELECTOR_SCENE_SUPPORTS_SANDBOX: Record<NodeSelectorScene, boolean> = {
|
||||
[NodeSelectorScene.Workflow]: true,
|
||||
[NodeSelectorScene.Chatflow]: true,
|
||||
[NodeSelectorScene.RagPipeline]: false,
|
||||
[NodeSelectorScene.Subgraph]: true,
|
||||
}
|
||||
|
||||
type NodeAvailabilityRule = {
|
||||
sandboxOnly?: boolean
|
||||
hiddenWhenSandboxEnabled?: boolean
|
||||
hiddenInScenes?: NodeSelectorScene[]
|
||||
}
|
||||
|
||||
export const NODE_SELECTOR_AVAILABILITY_RULES: Partial<Record<BlockEnum, NodeAvailabilityRule>> = {
|
||||
[BlockEnum.Command]: { sandboxOnly: true },
|
||||
[BlockEnum.FileUpload]: { sandboxOnly: true },
|
||||
[BlockEnum.Agent]: { hiddenWhenSandboxEnabled: true },
|
||||
[BlockEnum.HumanInput]: { hiddenInScenes: [NodeSelectorScene.RagPipeline] },
|
||||
}
|
||||
|
||||
export type NodeSelectorAvailabilityContext = {
|
||||
scene: NodeSelectorScene
|
||||
sandboxMode: NodeSelectorSandboxMode
|
||||
}
|
||||
|
||||
type BuildNodeSelectorAvailabilityContextProps = {
|
||||
scene: NodeSelectorScene
|
||||
isSandboxRuntime?: boolean
|
||||
isSandboxFeatureEnabled?: boolean
|
||||
supportsSandbox?: boolean
|
||||
}
|
||||
|
||||
const resolveSandboxMode = ({
|
||||
scene,
|
||||
isSandboxRuntime = false,
|
||||
isSandboxFeatureEnabled = false,
|
||||
supportsSandbox = NODE_SELECTOR_SCENE_SUPPORTS_SANDBOX[scene],
|
||||
}: BuildNodeSelectorAvailabilityContextProps): NodeSelectorSandboxMode => {
|
||||
if (!supportsSandbox)
|
||||
return NodeSelectorSandboxMode.Unsupported
|
||||
|
||||
return (isSandboxRuntime || isSandboxFeatureEnabled)
|
||||
? NodeSelectorSandboxMode.Enabled
|
||||
: NodeSelectorSandboxMode.Disabled
|
||||
}
|
||||
|
||||
export const buildNodeSelectorAvailabilityContext = ({
|
||||
scene,
|
||||
isSandboxRuntime,
|
||||
isSandboxFeatureEnabled,
|
||||
supportsSandbox,
|
||||
}: BuildNodeSelectorAvailabilityContextProps): NodeSelectorAvailabilityContext => {
|
||||
return {
|
||||
scene,
|
||||
sandboxMode: resolveSandboxMode({
|
||||
scene,
|
||||
isSandboxRuntime,
|
||||
isSandboxFeatureEnabled,
|
||||
supportsSandbox,
|
||||
}),
|
||||
}
|
||||
}
|
||||
|
||||
export const isNodeAvailableInSelector = (
|
||||
nodeType: BlockEnum,
|
||||
{ scene, sandboxMode }: NodeSelectorAvailabilityContext,
|
||||
) => {
|
||||
const rule = NODE_SELECTOR_AVAILABILITY_RULES[nodeType]
|
||||
if (!rule)
|
||||
return true
|
||||
|
||||
const sandboxEnabled = sandboxMode === NodeSelectorSandboxMode.Enabled
|
||||
if (rule.sandboxOnly && !sandboxEnabled)
|
||||
return false
|
||||
|
||||
if (rule.hiddenWhenSandboxEnabled && sandboxEnabled)
|
||||
return false
|
||||
|
||||
if (rule.hiddenInScenes?.includes(scene))
|
||||
return false
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
type NodeLike = {
|
||||
metaData: {
|
||||
type: BlockEnum
|
||||
}
|
||||
}
|
||||
|
||||
export const filterNodesForSelector = <T extends NodeLike>(
|
||||
nodes: T[],
|
||||
context: NodeSelectorAvailabilityContext,
|
||||
) => {
|
||||
const filteredNodes = nodes.filter(node => isNodeAvailableInSelector(node.metaData.type, context))
|
||||
if (filteredNodes.length === nodes.length)
|
||||
return nodes
|
||||
|
||||
return filteredNodes
|
||||
}
|
||||
Reference in New Issue
Block a user