mirror of
https://github.com/langgenius/dify.git
synced 2026-10-04 13:47:55 +08:00
perf(workflow): lazy load node configuration panels (#42970)
This commit is contained in:
1 parent
7020120197
commit
5f6bb5e6d3
13 files changed
+337
-105
No files matched your search
@@ -3179,7 +3179,7 @@
|
||||
},
|
||||
"web/app/components/workflow/nodes/index.tsx": {
|
||||
"eslint-react/static-components": {
|
||||
"count": 4
|
||||
"count": 2
|
||||
}
|
||||
},
|
||||
"web/app/components/workflow/nodes/iteration/node.tsx": {
|
||||
|
||||
@@ -3,13 +3,17 @@ import type { Node as WorkflowNode } from '../../types'
|
||||
import { render, screen } from '@testing-library/react'
|
||||
import { CUSTOM_NODE } from '../../constants'
|
||||
import { BlockEnum } from '../../types'
|
||||
import CustomNode, { Panel } from '../index'
|
||||
import CustomNode from '../index'
|
||||
import Panel from '../panel'
|
||||
|
||||
vi.mock('../components', () => ({
|
||||
NodeComponentMap: {
|
||||
[BlockEnum.Start]: () => <div>start-node-component</div>,
|
||||
[BlockEnum.StartPlaceholder]: () => <div>start-placeholder-node-component</div>,
|
||||
},
|
||||
}))
|
||||
|
||||
vi.mock('../panel-components', () => ({
|
||||
PanelComponentMap: {
|
||||
[BlockEnum.Start]: () => <div>start-panel-component</div>,
|
||||
[BlockEnum.StartPlaceholder]: () => <div>start-placeholder-panel-component</div>,
|
||||
|
||||
@@ -0,0 +1,121 @@
|
||||
import type { ComponentType } from 'react'
|
||||
import { act, render, screen } from '@testing-library/react'
|
||||
import userEvent from '@testing-library/user-event'
|
||||
import { useState } from 'react'
|
||||
import { lazyPanel } from '../lazy-panel'
|
||||
|
||||
type EditorProps = { title: string }
|
||||
function Editor({ title }: EditorProps) {
|
||||
const [draft, setDraft] = useState('')
|
||||
return (
|
||||
<label>
|
||||
{title}
|
||||
<input value={draft} onChange={(event) => setDraft(event.target.value)} />
|
||||
</label>
|
||||
)
|
||||
}
|
||||
|
||||
function deferredPanel() {
|
||||
let resolve!: (value: { default: ComponentType<EditorProps> }) => void
|
||||
const promise = new Promise<{ default: ComponentType<EditorProps> }>((res) => {
|
||||
resolve = res
|
||||
})
|
||||
return { promise, resolve }
|
||||
}
|
||||
|
||||
describe('lazy node panels', () => {
|
||||
it('loads only when selected and preserves an unsaved edit when node props update', async () => {
|
||||
const user = userEvent.setup()
|
||||
const pending = deferredPanel()
|
||||
const load = vi.fn(() => pending.promise)
|
||||
const Panel = lazyPanel(load)
|
||||
const { rerender } = render(<div />)
|
||||
expect(load).not.toHaveBeenCalled()
|
||||
|
||||
rerender(<Panel title="Prompt" />)
|
||||
expect(screen.getByRole('status', { name: 'common.loading' })).toBeInTheDocument()
|
||||
await act(async () => pending.resolve({ default: Editor }))
|
||||
await user.type(screen.getByRole('textbox', { name: 'Prompt' }), 'Unsaved prompt')
|
||||
|
||||
rerender(<Panel title="Updated prompt" />)
|
||||
expect(screen.getByRole('textbox', { name: 'Updated prompt' })).toHaveValue('Unsaved prompt')
|
||||
expect(load).toHaveBeenCalledTimes(1)
|
||||
})
|
||||
|
||||
it('does not replace the selected panel when a previous selection finishes loading', async () => {
|
||||
const first = deferredPanel()
|
||||
const second = deferredPanel()
|
||||
const FirstPanel = lazyPanel(() => first.promise)
|
||||
const SecondPanel = lazyPanel(() => second.promise)
|
||||
const { rerender } = render(<FirstPanel title="First node" />)
|
||||
|
||||
rerender(<SecondPanel title="Second node" />)
|
||||
await act(async () => second.resolve({ default: Editor }))
|
||||
expect(screen.getByRole('textbox', { name: 'Second node' })).toBeInTheDocument()
|
||||
await act(async () => first.resolve({ default: Editor }))
|
||||
expect(screen.queryByRole('textbox', { name: 'First node' })).not.toBeInTheDocument()
|
||||
expect(screen.getByRole('textbox', { name: 'Second node' })).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('keeps the surrounding editor available and retries a failed panel import', async () => {
|
||||
const user = userEvent.setup()
|
||||
const consoleError = vi.spyOn(console, 'error').mockImplementation(() => {})
|
||||
const load = vi
|
||||
.fn<() => Promise<{ default: ComponentType<EditorProps> }>>()
|
||||
.mockRejectedValueOnce(new Error('Chunk unavailable'))
|
||||
.mockResolvedValueOnce({ default: Editor })
|
||||
const Panel = lazyPanel(load)
|
||||
try {
|
||||
render(
|
||||
<>
|
||||
<button type="button">Close panel</button>
|
||||
<Panel title="Recovered panel" />
|
||||
</>,
|
||||
)
|
||||
await user.click(await screen.findByRole('button', { name: 'common.errorBoundary.tryAgain' }))
|
||||
expect(await screen.findByRole('textbox', { name: 'Recovered panel' })).toBeInTheDocument()
|
||||
expect(screen.getByRole('button', { name: 'Close panel' })).toBeEnabled()
|
||||
expect(load).toHaveBeenCalledTimes(2)
|
||||
} finally {
|
||||
consoleError.mockRestore()
|
||||
}
|
||||
})
|
||||
|
||||
it.each(['reopen', 'switch'] as const)(
|
||||
'retains a successful import retry across a panel %s',
|
||||
async (transition) => {
|
||||
const user = userEvent.setup()
|
||||
const consoleError = vi.spyOn(console, 'error').mockImplementation(() => {})
|
||||
const load = vi
|
||||
.fn<() => Promise<{ default: ComponentType<EditorProps> }>>()
|
||||
.mockRejectedValueOnce(new Error('Chunk unavailable'))
|
||||
.mockResolvedValue({ default: Editor })
|
||||
const Panel = lazyPanel(load)
|
||||
try {
|
||||
const { rerender } = render(<Panel key="first-node" title="First node" />)
|
||||
await user.click(
|
||||
await screen.findByRole('button', { name: 'common.errorBoundary.tryAgain' }),
|
||||
)
|
||||
expect(await screen.findByRole('textbox', { name: 'First node' })).toBeInTheDocument()
|
||||
|
||||
if (transition === 'reopen') {
|
||||
rerender(<div />)
|
||||
expect(screen.queryByRole('textbox')).not.toBeInTheDocument()
|
||||
}
|
||||
const nextTitle = transition === 'reopen' ? 'First node' : 'Second node'
|
||||
const nextKey = transition === 'reopen' ? 'first-node' : 'second-node'
|
||||
rerender(<Panel key={nextKey} title={nextTitle} />)
|
||||
|
||||
const input = await screen.findByRole('textbox', { name: nextTitle })
|
||||
await user.type(input, 'Recovered edit')
|
||||
expect(input).toHaveValue('Recovered edit')
|
||||
expect(
|
||||
screen.queryByRole('button', { name: 'common.errorBoundary.tryAgain' }),
|
||||
).not.toBeInTheDocument()
|
||||
expect(load).toHaveBeenCalledTimes(2)
|
||||
} finally {
|
||||
consoleError.mockRestore()
|
||||
}
|
||||
},
|
||||
)
|
||||
})
|
||||
@@ -0,0 +1,55 @@
|
||||
import type { ReactElement } from 'react'
|
||||
import type { Node } from '../../types'
|
||||
import { render, screen } from '@testing-library/react'
|
||||
import { cloneElement } from 'react'
|
||||
import { CUSTOM_NODE } from '../../constants'
|
||||
import { BlockEnum } from '../../types'
|
||||
import Panel from '../panel'
|
||||
|
||||
type InjectedProps = { id: string; data: Node['data'] }
|
||||
const loads = vi.hoisted(() => ({ legacy: vi.fn(), v2: vi.fn() }))
|
||||
vi.mock('../agent/panel', () => {
|
||||
loads.legacy()
|
||||
return {
|
||||
default: ({ id, data }: InjectedProps) => (
|
||||
<div>
|
||||
Legacy: {id} {data.title}
|
||||
</div>
|
||||
),
|
||||
}
|
||||
})
|
||||
vi.mock('../agent-v2/panel', () => {
|
||||
loads.v2()
|
||||
return {
|
||||
AgentV2Panel: ({ id, data }: InjectedProps) => (
|
||||
<div>
|
||||
V2: {id} {data.title}
|
||||
</div>
|
||||
),
|
||||
}
|
||||
})
|
||||
// The shell owns running and resizing. Preserve its node-prop injection contract
|
||||
// while exercising the real registry, lazy loaders, and Agent discriminator.
|
||||
vi.mock('../_base/components/workflow-panel', () => ({
|
||||
default: ({ children, ...props }: InjectedProps & { children: ReactElement<InjectedProps> }) =>
|
||||
cloneElement(children, props),
|
||||
}))
|
||||
|
||||
it('loads the correct Agent variant for legacy and explicit V2 node types', async () => {
|
||||
const data: Node['data'] = { title: 'Agent', desc: '', type: BlockEnum.Agent }
|
||||
expect(loads.legacy).not.toHaveBeenCalled()
|
||||
expect(loads.v2).not.toHaveBeenCalled()
|
||||
const { rerender } = render(<Panel id="agent-1" type={CUSTOM_NODE} data={data} />)
|
||||
expect(await screen.findByText('Legacy: agent-1 Agent')).toBeInTheDocument()
|
||||
expect(loads.v2).not.toHaveBeenCalled()
|
||||
|
||||
const v2Data = { ...data, agent_node_kind: 'dify_agent', version: '2' }
|
||||
rerender(<Panel id="agent-1" type={CUSTOM_NODE} data={v2Data} />)
|
||||
expect(await screen.findByText('V2: agent-1 Agent')).toBeInTheDocument()
|
||||
expect(screen.queryByText('Legacy: agent-1 Agent')).not.toBeInTheDocument()
|
||||
|
||||
rerender(<Panel id="agent-2" type={CUSTOM_NODE} data={{ ...v2Data, type: BlockEnum.AgentV2 }} />)
|
||||
expect(await screen.findByText('V2: agent-2 Agent')).toBeInTheDocument()
|
||||
expect(loads.legacy).toHaveBeenCalledTimes(1)
|
||||
expect(loads.v2).toHaveBeenCalledTimes(1)
|
||||
})
|
||||
@@ -2,65 +2,35 @@ import type { ComponentProps, ComponentType } from 'react'
|
||||
import { createElement } from 'react'
|
||||
import { BlockEnum } from '../types'
|
||||
import { AgentV2Node } from './agent-v2/node'
|
||||
import { AgentV2Panel } from './agent-v2/panel'
|
||||
import { isAgentV2NodeData } from './agent-v2/types'
|
||||
import AgentNode from './agent/node'
|
||||
import AgentPanel from './agent/panel'
|
||||
import AnswerNode from './answer/node'
|
||||
import AnswerPanel from './answer/panel'
|
||||
import AssignerNode from './assigner/node'
|
||||
import AssignerPanel from './assigner/panel'
|
||||
import CodeNode from './code/node'
|
||||
import CodePanel from './code/panel'
|
||||
import DataSourceNode from './data-source/node'
|
||||
import DataSourcePanel from './data-source/panel'
|
||||
import DocExtractorNode from './document-extractor/node'
|
||||
import DocExtractorPanel from './document-extractor/panel'
|
||||
import EndNode from './end/node'
|
||||
import EndPanel from './end/panel'
|
||||
import HttpNode from './http/node'
|
||||
import HttpPanel from './http/panel'
|
||||
import HumanInputNode from './human-input/node'
|
||||
import HumanInputPanel from './human-input/panel'
|
||||
import IfElseNode from './if-else/node'
|
||||
import IfElsePanel from './if-else/panel'
|
||||
import IterationNode from './iteration/node'
|
||||
import IterationPanel from './iteration/panel'
|
||||
import KnowledgeBaseNode from './knowledge-base/node'
|
||||
import KnowledgeBasePanel from './knowledge-base/panel'
|
||||
import KnowledgeRetrievalNode from './knowledge-retrieval/node'
|
||||
import KnowledgeRetrievalPanel from './knowledge-retrieval/panel'
|
||||
import ListFilterNode from './list-operator/node'
|
||||
import ListFilterPanel from './list-operator/panel'
|
||||
import LLMNode from './llm/node'
|
||||
import LLMPanel from './llm/panel'
|
||||
import LoopNode from './loop/node'
|
||||
import LoopPanel from './loop/panel'
|
||||
import ParameterExtractorNode from './parameter-extractor/node'
|
||||
import ParameterExtractorPanel from './parameter-extractor/panel'
|
||||
import QuestionClassifierNode from './question-classifier/node'
|
||||
import QuestionClassifierPanel from './question-classifier/panel'
|
||||
import StartPlaceholderNode from './start-placeholder/node'
|
||||
import StartPlaceholderPanel from './start-placeholder/panel'
|
||||
import StartNode from './start/node'
|
||||
import StartPanel from './start/panel'
|
||||
import TemplateTransformNode from './template-transform/node'
|
||||
import TemplateTransformPanel from './template-transform/panel'
|
||||
import ToolNode from './tool/node'
|
||||
import ToolPanel from './tool/panel'
|
||||
import TriggerPluginNode from './trigger-plugin/node'
|
||||
import TriggerPluginPanel from './trigger-plugin/panel'
|
||||
import TriggerScheduleNode from './trigger-schedule/node'
|
||||
import TriggerSchedulePanel from './trigger-schedule/panel'
|
||||
import TriggerWebhookNode from './trigger-webhook/node'
|
||||
import TriggerWebhookPanel from './trigger-webhook/panel'
|
||||
import VariableAssignerNode from './variable-assigner/node'
|
||||
import VariableAssignerPanel from './variable-assigner/panel'
|
||||
|
||||
type WorkflowAgentNodeProps = ComponentProps<typeof AgentNode> | ComponentProps<typeof AgentV2Node>
|
||||
type WorkflowAgentPanelProps =
|
||||
| ComponentProps<typeof AgentPanel>
|
||||
| ComponentProps<typeof AgentV2Panel>
|
||||
type WorkflowComponentMap = Record<string, ComponentType<Record<string, never>>>
|
||||
|
||||
function WorkflowAgentNode(props: WorkflowAgentNodeProps) {
|
||||
@@ -70,13 +40,6 @@ function WorkflowAgentNode(props: WorkflowAgentNodeProps) {
|
||||
return createElement(AgentNode, props as ComponentProps<typeof AgentNode>)
|
||||
}
|
||||
|
||||
function WorkflowAgentPanel(props: WorkflowAgentPanelProps) {
|
||||
if (isAgentV2NodeData(props.data))
|
||||
return createElement(AgentV2Panel, props as ComponentProps<typeof AgentV2Panel>)
|
||||
|
||||
return createElement(AgentPanel, props as ComponentProps<typeof AgentPanel>)
|
||||
}
|
||||
|
||||
export const NodeComponentMap = {
|
||||
[BlockEnum.Start]: StartNode,
|
||||
[BlockEnum.StartPlaceholder]: StartPlaceholderNode,
|
||||
@@ -107,34 +70,3 @@ export const NodeComponentMap = {
|
||||
[BlockEnum.TriggerWebhook]: TriggerWebhookNode,
|
||||
[BlockEnum.TriggerPlugin]: TriggerPluginNode,
|
||||
} as unknown as WorkflowComponentMap
|
||||
|
||||
export const PanelComponentMap = {
|
||||
[BlockEnum.Start]: StartPanel,
|
||||
[BlockEnum.StartPlaceholder]: StartPlaceholderPanel,
|
||||
[BlockEnum.End]: EndPanel,
|
||||
[BlockEnum.Answer]: AnswerPanel,
|
||||
[BlockEnum.LLM]: LLMPanel,
|
||||
[BlockEnum.KnowledgeRetrieval]: KnowledgeRetrievalPanel,
|
||||
[BlockEnum.QuestionClassifier]: QuestionClassifierPanel,
|
||||
[BlockEnum.IfElse]: IfElsePanel,
|
||||
[BlockEnum.Code]: CodePanel,
|
||||
[BlockEnum.TemplateTransform]: TemplateTransformPanel,
|
||||
[BlockEnum.HttpRequest]: HttpPanel,
|
||||
[BlockEnum.Tool]: ToolPanel,
|
||||
[BlockEnum.VariableAssigner]: VariableAssignerPanel,
|
||||
[BlockEnum.VariableAggregator]: VariableAssignerPanel,
|
||||
[BlockEnum.Assigner]: AssignerPanel,
|
||||
[BlockEnum.ParameterExtractor]: ParameterExtractorPanel,
|
||||
[BlockEnum.Iteration]: IterationPanel,
|
||||
[BlockEnum.Loop]: LoopPanel,
|
||||
[BlockEnum.DocExtractor]: DocExtractorPanel,
|
||||
[BlockEnum.ListFilter]: ListFilterPanel,
|
||||
[BlockEnum.Agent]: WorkflowAgentPanel,
|
||||
[BlockEnum.AgentV2]: AgentV2Panel,
|
||||
[BlockEnum.DataSource]: DataSourcePanel,
|
||||
[BlockEnum.KnowledgeBase]: KnowledgeBasePanel,
|
||||
[BlockEnum.HumanInput]: HumanInputPanel,
|
||||
[BlockEnum.TriggerSchedule]: TriggerSchedulePanel,
|
||||
[BlockEnum.TriggerWebhook]: TriggerWebhookPanel,
|
||||
[BlockEnum.TriggerPlugin]: TriggerPluginPanel,
|
||||
} as unknown as WorkflowComponentMap
|
||||
@@ -1,10 +1,7 @@
|
||||
import type { NodeProps } from 'reactflow'
|
||||
import type { Node } from '../types'
|
||||
import { memo, useMemo } from 'react'
|
||||
import { CUSTOM_NODE } from '../constants'
|
||||
import BasePanel from './_base/components/workflow-panel'
|
||||
import BaseNode from './_base/node'
|
||||
import { NodeComponentMap, PanelComponentMap } from './components'
|
||||
import { NodeComponentMap } from './components'
|
||||
|
||||
const CustomNode = (props: NodeProps) => {
|
||||
const nodeData = props.data
|
||||
@@ -20,30 +17,4 @@ const CustomNode = (props: NodeProps) => {
|
||||
}
|
||||
CustomNode.displayName = 'CustomNode'
|
||||
|
||||
type PanelProps = {
|
||||
type: Node['type']
|
||||
id: Node['id']
|
||||
data: Node['data']
|
||||
}
|
||||
export const Panel = memo((props: PanelProps) => {
|
||||
const nodeClass = props.type
|
||||
const nodeData = props.data
|
||||
const PanelComponent = useMemo(() => {
|
||||
if (nodeClass === CUSTOM_NODE) return PanelComponentMap[nodeData.type]
|
||||
return () => null
|
||||
}, [nodeClass, nodeData.type])!
|
||||
|
||||
if (nodeClass === CUSTOM_NODE) {
|
||||
return (
|
||||
<BasePanel key={`${props.id}-${nodeData.type}`} id={props.id} data={props.data}>
|
||||
<PanelComponent />
|
||||
</BasePanel>
|
||||
)
|
||||
}
|
||||
|
||||
return null
|
||||
})
|
||||
|
||||
Panel.displayName = 'Panel'
|
||||
|
||||
export default memo(CustomNode)
|
||||
@@ -0,0 +1,28 @@
|
||||
import type { ComponentType } from 'react'
|
||||
import { createElement, lazy, Suspense, useState } from 'react'
|
||||
import ErrorBoundary from '@/app/components/base/error-boundary'
|
||||
import PanelLoading from './panel-loading'
|
||||
|
||||
// Keep the component identity stable while BasePanel injects updated node data
|
||||
// and run parameters. Share explicit retries with future mounts so reopening a
|
||||
// recovered panel does not reuse the original lazy component's cached rejection.
|
||||
export function lazyPanel<Props extends object>(
|
||||
load: () => Promise<{ default: ComponentType<Props> }>,
|
||||
) {
|
||||
let CurrentPanel = lazy(load)
|
||||
|
||||
return function LazyPanel(props: Props) {
|
||||
const [Panel, setPanel] = useState(() => CurrentPanel)
|
||||
|
||||
const retry = () => {
|
||||
CurrentPanel = lazy(load)
|
||||
setPanel(() => CurrentPanel)
|
||||
}
|
||||
|
||||
return (
|
||||
<ErrorBoundary onReset={retry}>
|
||||
<Suspense fallback={<PanelLoading />}>{createElement(Panel, props)}</Suspense>
|
||||
</ErrorBoundary>
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,76 @@
|
||||
import type { ComponentProps, ComponentType } from 'react'
|
||||
import { createElement } from 'react'
|
||||
import { BlockEnum } from '../types'
|
||||
import { isAgentV2NodeData } from './agent-v2/types'
|
||||
import { lazyPanel } from './lazy-panel'
|
||||
|
||||
const AgentV2Panel = lazyPanel(async () => {
|
||||
const { AgentV2Panel } = await import('./agent-v2/panel')
|
||||
return { default: AgentV2Panel }
|
||||
})
|
||||
const AgentPanel = lazyPanel(() => import('./agent/panel'))
|
||||
const AnswerPanel = lazyPanel(() => import('./answer/panel'))
|
||||
const AssignerPanel = lazyPanel(() => import('./assigner/panel'))
|
||||
const CodePanel = lazyPanel(() => import('./code/panel'))
|
||||
const DataSourcePanel = lazyPanel(() => import('./data-source/panel'))
|
||||
const DocExtractorPanel = lazyPanel(() => import('./document-extractor/panel'))
|
||||
const EndPanel = lazyPanel(() => import('./end/panel'))
|
||||
const HttpPanel = lazyPanel(() => import('./http/panel'))
|
||||
const HumanInputPanel = lazyPanel(() => import('./human-input/panel'))
|
||||
const IfElsePanel = lazyPanel(() => import('./if-else/panel'))
|
||||
const IterationPanel = lazyPanel(() => import('./iteration/panel'))
|
||||
const KnowledgeBasePanel = lazyPanel(() => import('./knowledge-base/panel'))
|
||||
const KnowledgeRetrievalPanel = lazyPanel(() => import('./knowledge-retrieval/panel'))
|
||||
const ListFilterPanel = lazyPanel(() => import('./list-operator/panel'))
|
||||
const LLMPanel = lazyPanel(() => import('./llm/panel'))
|
||||
const LoopPanel = lazyPanel(() => import('./loop/panel'))
|
||||
const ParameterExtractorPanel = lazyPanel(() => import('./parameter-extractor/panel'))
|
||||
const QuestionClassifierPanel = lazyPanel(() => import('./question-classifier/panel'))
|
||||
const StartPlaceholderPanel = lazyPanel(() => import('./start-placeholder/panel'))
|
||||
const StartPanel = lazyPanel(() => import('./start/panel'))
|
||||
const TemplateTransformPanel = lazyPanel(() => import('./template-transform/panel'))
|
||||
const ToolPanel = lazyPanel(() => import('./tool/panel'))
|
||||
const TriggerPluginPanel = lazyPanel(() => import('./trigger-plugin/panel'))
|
||||
const TriggerSchedulePanel = lazyPanel(() => import('./trigger-schedule/panel'))
|
||||
const TriggerWebhookPanel = lazyPanel(() => import('./trigger-webhook/panel'))
|
||||
const VariableAssignerPanel = lazyPanel(() => import('./variable-assigner/panel'))
|
||||
|
||||
function WorkflowAgentPanel(
|
||||
props: ComponentProps<typeof AgentPanel> | ComponentProps<typeof AgentV2Panel>,
|
||||
) {
|
||||
if (isAgentV2NodeData(props.data))
|
||||
return createElement(AgentV2Panel, props as ComponentProps<typeof AgentV2Panel>)
|
||||
|
||||
return createElement(AgentPanel, props as ComponentProps<typeof AgentPanel>)
|
||||
}
|
||||
|
||||
export const PanelComponentMap = {
|
||||
[BlockEnum.Start]: StartPanel,
|
||||
[BlockEnum.StartPlaceholder]: StartPlaceholderPanel,
|
||||
[BlockEnum.End]: EndPanel,
|
||||
[BlockEnum.Answer]: AnswerPanel,
|
||||
[BlockEnum.LLM]: LLMPanel,
|
||||
[BlockEnum.KnowledgeRetrieval]: KnowledgeRetrievalPanel,
|
||||
[BlockEnum.QuestionClassifier]: QuestionClassifierPanel,
|
||||
[BlockEnum.IfElse]: IfElsePanel,
|
||||
[BlockEnum.Code]: CodePanel,
|
||||
[BlockEnum.TemplateTransform]: TemplateTransformPanel,
|
||||
[BlockEnum.HttpRequest]: HttpPanel,
|
||||
[BlockEnum.Tool]: ToolPanel,
|
||||
[BlockEnum.VariableAssigner]: VariableAssignerPanel,
|
||||
[BlockEnum.VariableAggregator]: VariableAssignerPanel,
|
||||
[BlockEnum.Assigner]: AssignerPanel,
|
||||
[BlockEnum.ParameterExtractor]: ParameterExtractorPanel,
|
||||
[BlockEnum.Iteration]: IterationPanel,
|
||||
[BlockEnum.Loop]: LoopPanel,
|
||||
[BlockEnum.DocExtractor]: DocExtractorPanel,
|
||||
[BlockEnum.ListFilter]: ListFilterPanel,
|
||||
[BlockEnum.Agent]: WorkflowAgentPanel,
|
||||
[BlockEnum.AgentV2]: AgentV2Panel,
|
||||
[BlockEnum.DataSource]: DataSourcePanel,
|
||||
[BlockEnum.KnowledgeBase]: KnowledgeBasePanel,
|
||||
[BlockEnum.HumanInput]: HumanInputPanel,
|
||||
[BlockEnum.TriggerSchedule]: TriggerSchedulePanel,
|
||||
[BlockEnum.TriggerWebhook]: TriggerWebhookPanel,
|
||||
[BlockEnum.TriggerPlugin]: TriggerPluginPanel,
|
||||
} as unknown as Record<string, ComponentType<Record<string, never>>>
|
||||
@@ -0,0 +1,17 @@
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { SkeletonContainer, SkeletonRectangle } from '@/app/components/base/skeleton'
|
||||
|
||||
function PanelLoading() {
|
||||
const { t } = useTranslation(['common'])
|
||||
|
||||
return (
|
||||
<SkeletonContainer role="status" aria-label={t(($) => $.loading)} className="p-4">
|
||||
<SkeletonRectangle className="h-4 w-24" />
|
||||
<SkeletonRectangle className="h-20 w-full" />
|
||||
<SkeletonRectangle className="h-4 w-24" />
|
||||
<SkeletonRectangle className="h-20 w-full" />
|
||||
</SkeletonContainer>
|
||||
)
|
||||
}
|
||||
|
||||
export default PanelLoading
|
||||
@@ -0,0 +1,28 @@
|
||||
import type { Node } from '../types'
|
||||
import { memo } from 'react'
|
||||
import { CUSTOM_NODE } from '../constants'
|
||||
import BasePanel from './_base/components/workflow-panel'
|
||||
import { PanelComponentMap } from './panel-components'
|
||||
|
||||
type PanelProps = {
|
||||
type: Node['type']
|
||||
id: Node['id']
|
||||
data: Node['data']
|
||||
}
|
||||
const Panel = memo((props: PanelProps) => {
|
||||
const nodeClass = props.type
|
||||
const nodeData = props.data
|
||||
if (nodeClass !== CUSTOM_NODE) return null
|
||||
|
||||
const PanelComponent = PanelComponentMap[nodeData.type]!
|
||||
|
||||
return (
|
||||
<BasePanel key={`${props.id}-${nodeData.type}`} id={props.id} data={props.data}>
|
||||
<PanelComponent />
|
||||
</BasePanel>
|
||||
)
|
||||
})
|
||||
|
||||
Panel.displayName = 'Panel'
|
||||
|
||||
export default Panel
|
||||
@@ -87,8 +87,8 @@ vi.mock('../../store', () => ({
|
||||
useStore: <T,>(selector: (state: MockPanelStoreState) => T) => selector(mockPanelStoreState),
|
||||
}))
|
||||
|
||||
vi.mock('../../nodes', () => ({
|
||||
Panel: ({ id, data }: { id: string; data: MockNodeData }) => (
|
||||
vi.mock('../../nodes/panel', () => ({
|
||||
default: ({ id, data }: { id: string; data: MockNodeData }) => (
|
||||
<div data-testid="node-panel">{`${id}:${data.title || 'untitled'}`}</div>
|
||||
),
|
||||
}))
|
||||
|
||||
@@ -35,8 +35,8 @@ vi.mock('@/app/components/workflow/panel/inputs-panel', () => ({ default: () =>
|
||||
vi.mock('@/app/components/workflow/panel/env-panel', () => ({ default: () => null }))
|
||||
// Node details do not own the preview width limit. Node-panel compression is
|
||||
// covered separately at the real BasePanel boundary.
|
||||
vi.mock('@/app/components/workflow/nodes', () => ({
|
||||
Panel: () => <div>Selected node details</div>,
|
||||
vi.mock('@/app/components/workflow/nodes/panel', () => ({
|
||||
default: () => <div>Selected node details</div>,
|
||||
}))
|
||||
|
||||
function SelectedNodePanel({ children }: { children: ReactNode }) {
|
||||
|
||||
@@ -5,7 +5,7 @@ import dynamic from 'next/dynamic'
|
||||
import { memo, useEffect, useRef } from 'react'
|
||||
import { useStore as useReactflow } from 'reactflow'
|
||||
import { useShallow } from 'zustand/react/shallow'
|
||||
import { Panel as NodePanel } from '../nodes'
|
||||
import NodePanel from '../nodes/panel'
|
||||
import { useStore } from '../store'
|
||||
import EnvPanel from './env-panel'
|
||||
import { getPreviewPanelMaxWidth } from './panel-width'
|
||||
|
||||
Reference in new issue
Block a user