| import logging | import logging | ||||
| import time | |||||
| from enum import Enum | from enum import Enum | ||||
| from threading import Lock | from threading import Lock | ||||
| from typing import Literal, Optional | |||||
| from typing import Optional | |||||
| from httpx import Timeout, get, post | |||||
| from httpx import Timeout, post | |||||
| from pydantic import BaseModel | from pydantic import BaseModel | ||||
| from yarl import URL | from yarl import URL | ||||
| from configs import dify_config | from configs import dify_config | ||||
| from core.helper.code_executor.entities import CodeDependency | |||||
| from core.helper.code_executor.javascript.javascript_transformer import NodeJsTemplateTransformer | from core.helper.code_executor.javascript.javascript_transformer import NodeJsTemplateTransformer | ||||
| from core.helper.code_executor.jinja2.jinja2_transformer import Jinja2TemplateTransformer | from core.helper.code_executor.jinja2.jinja2_transformer import Jinja2TemplateTransformer | ||||
| from core.helper.code_executor.python3.python3_transformer import Python3TemplateTransformer | from core.helper.code_executor.python3.python3_transformer import Python3TemplateTransformer | ||||
| def execute_code(cls, | def execute_code(cls, | ||||
| language: CodeLanguage, | language: CodeLanguage, | ||||
| preload: str, | preload: str, | ||||
| code: str, | |||||
| dependencies: Optional[list[CodeDependency]] = None) -> str: | |||||
| code: str) -> str: | |||||
| """ | """ | ||||
| Execute code | Execute code | ||||
| :param language: code language | :param language: code language | ||||
| 'enable_network': True | 'enable_network': True | ||||
| } | } | ||||
| if dependencies: | |||||
| data['dependencies'] = [dependency.model_dump() for dependency in dependencies] | |||||
| try: | try: | ||||
| response = post(str(url), json=data, headers=headers, timeout=CODE_EXECUTION_TIMEOUT) | response = post(str(url), json=data, headers=headers, timeout=CODE_EXECUTION_TIMEOUT) | ||||
| if response.status_code == 503: | if response.status_code == 503: | ||||
| return response.data.stdout or '' | return response.data.stdout or '' | ||||
| @classmethod | @classmethod | ||||
| def execute_workflow_code_template(cls, language: CodeLanguage, code: str, inputs: dict, dependencies: Optional[list[CodeDependency]] = None) -> dict: | |||||
| def execute_workflow_code_template(cls, language: CodeLanguage, code: str, inputs: dict) -> dict: | |||||
| """ | """ | ||||
| Execute code | Execute code | ||||
| :param language: code language | :param language: code language | ||||
| if not template_transformer: | if not template_transformer: | ||||
| raise CodeExecutionException(f'Unsupported language {language}') | raise CodeExecutionException(f'Unsupported language {language}') | ||||
| runner, preload, dependencies = template_transformer.transform_caller(code, inputs, dependencies) | |||||
| runner, preload = template_transformer.transform_caller(code, inputs) | |||||
| try: | try: | ||||
| response = cls.execute_code(language, preload, runner, dependencies) | |||||
| response = cls.execute_code(language, preload, runner) | |||||
| except CodeExecutionException as e: | except CodeExecutionException as e: | ||||
| raise e | raise e | ||||
| return template_transformer.transform_response(response) | return template_transformer.transform_response(response) | ||||
| @classmethod | |||||
| def list_dependencies(cls, language: str) -> list[CodeDependency]: | |||||
| if language not in cls.supported_dependencies_languages: | |||||
| return [] | |||||
| with cls.dependencies_cache_lock: | |||||
| if language in cls.dependencies_cache: | |||||
| # check expiration | |||||
| dependencies = cls.dependencies_cache[language] | |||||
| if dependencies['expiration'] > time.time(): | |||||
| return dependencies['data'] | |||||
| # remove expired cache | |||||
| del cls.dependencies_cache[language] | |||||
| dependencies = cls._get_dependencies(language) | |||||
| with cls.dependencies_cache_lock: | |||||
| cls.dependencies_cache[language] = { | |||||
| 'data': dependencies, | |||||
| 'expiration': time.time() + 60 | |||||
| } | |||||
| return dependencies | |||||
| @classmethod | |||||
| def _get_dependencies(cls, language: Literal['python3']) -> list[CodeDependency]: | |||||
| """ | |||||
| List dependencies | |||||
| """ | |||||
| url = URL(CODE_EXECUTION_ENDPOINT) / 'v1' / 'sandbox' / 'dependencies' | |||||
| headers = { | |||||
| 'X-Api-Key': CODE_EXECUTION_API_KEY | |||||
| } | |||||
| running_language = cls.code_language_to_running_language.get(language) | |||||
| if isinstance(running_language, Enum): | |||||
| running_language = running_language.value | |||||
| data = { | |||||
| 'language': running_language, | |||||
| } | |||||
| try: | |||||
| response = get(str(url), params=data, headers=headers, timeout=CODE_EXECUTION_TIMEOUT) | |||||
| if response.status_code != 200: | |||||
| raise Exception(f'Failed to list dependencies, got status code {response.status_code}, please check if the sandbox service is running') | |||||
| response = response.json() | |||||
| dependencies = response.get('data', {}).get('dependencies', []) | |||||
| return [ | |||||
| CodeDependency(**dependency) for dependency in dependencies | |||||
| if dependency.get('name') not in Python3TemplateTransformer.get_standard_packages() | |||||
| ] | |||||
| except Exception as e: | |||||
| logger.exception(f'Failed to list dependencies: {e}') | |||||
| return [] | |||||
| from pydantic import BaseModel | from pydantic import BaseModel | ||||
| from core.helper.code_executor.code_executor import CodeExecutor | |||||
| class CodeNodeProvider(BaseModel): | class CodeNodeProvider(BaseModel): | ||||
| @staticmethod | @staticmethod | ||||
| """ | """ | ||||
| pass | pass | ||||
| @classmethod | |||||
| def get_default_available_packages(cls) -> list[dict]: | |||||
| return [p.model_dump() for p in CodeExecutor.list_dependencies(cls.get_language())] | |||||
| @classmethod | @classmethod | ||||
| def get_default_config(cls) -> dict: | def get_default_config(cls) -> dict: | ||||
| return { | return { | ||||
| "children": None | "children": None | ||||
| } | } | ||||
| } | } | ||||
| }, | |||||
| "available_dependencies": cls.get_default_available_packages(), | |||||
| } | |||||
| } | } |
| from pydantic import BaseModel | |||||
| class CodeDependency(BaseModel): | |||||
| name: str | |||||
| version: str |
| class Jinja2Formatter: | class Jinja2Formatter: | ||||
| @classmethod | @classmethod | ||||
| def format(cls, template: str, inputs: str) -> str: | |||||
| def format(cls, template: str, inputs: dict) -> str: | |||||
| """ | """ | ||||
| Format template | Format template | ||||
| :param template: template | :param template: template |
| from textwrap import dedent | from textwrap import dedent | ||||
| from core.helper.code_executor.python3.python3_transformer import Python3TemplateTransformer | |||||
| from core.helper.code_executor.template_transformer import TemplateTransformer | from core.helper.code_executor.template_transformer import TemplateTransformer | ||||
| class Jinja2TemplateTransformer(TemplateTransformer): | class Jinja2TemplateTransformer(TemplateTransformer): | ||||
| @classmethod | |||||
| def get_standard_packages(cls) -> set[str]: | |||||
| return {'jinja2'} | Python3TemplateTransformer.get_standard_packages() | |||||
| @classmethod | @classmethod | ||||
| def transform_response(cls, response: str) -> dict: | def transform_response(cls, response: str) -> dict: | ||||
| """ | """ |
| class Python3TemplateTransformer(TemplateTransformer): | class Python3TemplateTransformer(TemplateTransformer): | ||||
| @classmethod | |||||
| def get_standard_packages(cls) -> set[str]: | |||||
| return { | |||||
| 'base64', | |||||
| 'binascii', | |||||
| 'collections', | |||||
| 'datetime', | |||||
| 'functools', | |||||
| 'hashlib', | |||||
| 'hmac', | |||||
| 'itertools', | |||||
| 'json', | |||||
| 'math', | |||||
| 'operator', | |||||
| 'os', | |||||
| 'random', | |||||
| 're', | |||||
| 'string', | |||||
| 'sys', | |||||
| 'time', | |||||
| 'traceback', | |||||
| 'uuid', | |||||
| } | |||||
| @classmethod | @classmethod | ||||
| def get_runner_script(cls) -> str: | def get_runner_script(cls) -> str: | ||||
| runner_script = dedent(f""" | runner_script = dedent(f""" |
| import re | import re | ||||
| from abc import ABC, abstractmethod | from abc import ABC, abstractmethod | ||||
| from base64 import b64encode | from base64 import b64encode | ||||
| from typing import Optional | |||||
| from core.helper.code_executor.entities import CodeDependency | |||||
| class TemplateTransformer(ABC): | class TemplateTransformer(ABC): | ||||
| _result_tag: str = '<<RESULT>>' | _result_tag: str = '<<RESULT>>' | ||||
| @classmethod | @classmethod | ||||
| def get_standard_packages(cls) -> set[str]: | |||||
| return set() | |||||
| @classmethod | |||||
| def transform_caller(cls, code: str, inputs: dict, | |||||
| dependencies: Optional[list[CodeDependency]] = None) -> tuple[str, str, list[CodeDependency]]: | |||||
| def transform_caller(cls, code: str, inputs: dict) -> tuple[str, str]: | |||||
| """ | """ | ||||
| Transform code to python runner | Transform code to python runner | ||||
| :param code: code | :param code: code | ||||
| runner_script = cls.assemble_runner_script(code, inputs) | runner_script = cls.assemble_runner_script(code, inputs) | ||||
| preload_script = cls.get_preload_script() | preload_script = cls.get_preload_script() | ||||
| packages = dependencies or [] | |||||
| standard_packages = cls.get_standard_packages() | |||||
| for package in standard_packages: | |||||
| if package not in packages: | |||||
| packages.append(CodeDependency(name=package, version='')) | |||||
| packages = list({dep.name: dep for dep in packages if dep.name}.values()) | |||||
| return runner_script, preload_script, packages | |||||
| return runner_script, preload_script | |||||
| @classmethod | @classmethod | ||||
| def extract_result_str_from_response(cls, response: str) -> str: | def extract_result_str_from_response(cls, response: str) -> str: |
| language=code_language, | language=code_language, | ||||
| code=code, | code=code, | ||||
| inputs=variables, | inputs=variables, | ||||
| dependencies=node_data.dependencies | |||||
| ) | ) | ||||
| # Transform result | # Transform result |
| from pydantic import BaseModel | from pydantic import BaseModel | ||||
| from core.helper.code_executor.code_executor import CodeLanguage | from core.helper.code_executor.code_executor import CodeLanguage | ||||
| from core.helper.code_executor.entities import CodeDependency | |||||
| from core.workflow.entities.base_node_data_entities import BaseNodeData | from core.workflow.entities.base_node_data_entities import BaseNodeData | ||||
| from core.workflow.entities.variable_entities import VariableSelector | from core.workflow.entities.variable_entities import VariableSelector | ||||
| type: Literal['string', 'number', 'object', 'array[string]', 'array[number]', 'array[object]'] | type: Literal['string', 'number', 'object', 'array[string]', 'array[number]', 'array[object]'] | ||||
| children: Optional[dict[str, 'Output']] = None | children: Optional[dict[str, 'Output']] = None | ||||
| class Dependency(BaseModel): | |||||
| name: str | |||||
| version: str | |||||
| variables: list[VariableSelector] | variables: list[VariableSelector] | ||||
| code_language: Literal[CodeLanguage.PYTHON3, CodeLanguage.JAVASCRIPT] | code_language: Literal[CodeLanguage.PYTHON3, CodeLanguage.JAVASCRIPT] | ||||
| code: str | code: str | ||||
| outputs: dict[str, Output] | outputs: dict[str, Output] | ||||
| dependencies: Optional[list[CodeDependency]] = None | |||||
| dependencies: Optional[list[Dependency]] = None |
| from jinja2 import Template | from jinja2 import Template | ||||
| from core.helper.code_executor.code_executor import CodeExecutor, CodeLanguage | from core.helper.code_executor.code_executor import CodeExecutor, CodeLanguage | ||||
| from core.helper.code_executor.entities import CodeDependency | |||||
| MOCK = os.getenv('MOCK_SWITCH', 'false') == 'true' | MOCK = os.getenv('MOCK_SWITCH', 'false') == 'true' | ||||
| class MockedCodeExecutor: | class MockedCodeExecutor: | ||||
| @classmethod | @classmethod | ||||
| def invoke(cls, language: Literal['python3', 'javascript', 'jinja2'], | def invoke(cls, language: Literal['python3', 'javascript', 'jinja2'], | ||||
| code: str, inputs: dict, dependencies: Optional[list[CodeDependency]] = None) -> dict: | |||||
| code: str, inputs: dict) -> dict: | |||||
| # invoke directly | # invoke directly | ||||
| match language: | match language: | ||||
| case CodeLanguage.PYTHON3: | case CodeLanguage.PYTHON3: | ||||
| return { | return { | ||||
| "result": Template(code).render(inputs) | "result": Template(code).render(inputs) | ||||
| } | } | ||||
| case _: | |||||
| raise Exception("Language not supported") | |||||
| @pytest.fixture | @pytest.fixture | ||||
| def setup_code_executor_mock(request, monkeypatch: MonkeyPatch): | def setup_code_executor_mock(request, monkeypatch: MonkeyPatch): |
| inputs={'arg1': 'Hello', 'arg2': 'World'}) | inputs={'arg1': 'Hello', 'arg2': 'World'}) | ||||
| assert result == {'result': 'HelloWorld'} | assert result == {'result': 'HelloWorld'} | ||||
| def test_javascript_list_default_available_packages(): | |||||
| packages = JavascriptCodeProvider.get_default_available_packages() | |||||
| # no default packages available for javascript | |||||
| assert len(packages) == 0 | |||||
| def test_javascript_get_runner_script(): | def test_javascript_get_runner_script(): | ||||
| runner_script = NodeJsTemplateTransformer.get_runner_script() | runner_script = NodeJsTemplateTransformer.get_runner_script() | ||||
| assert runner_script.count(NodeJsTemplateTransformer._code_placeholder) == 1 | assert runner_script.count(NodeJsTemplateTransformer._code_placeholder) == 1 |
| assert result == {'result': 'HelloWorld'} | assert result == {'result': 'HelloWorld'} | ||||
| def test_python3_list_default_available_packages(): | |||||
| packages = Python3CodeProvider.get_default_available_packages() | |||||
| assert len(packages) > 0 | |||||
| assert {'requests', 'httpx'}.issubset(p['name'] for p in packages) | |||||
| # check JSON serializable | |||||
| assert len(str(json.dumps(packages))) > 0 | |||||
| def test_python3_get_runner_script(): | def test_python3_get_runner_script(): | ||||
| runner_script = Python3TemplateTransformer.get_runner_script() | runner_script = Python3TemplateTransformer.get_runner_script() | ||||
| assert runner_script.count(Python3TemplateTransformer._code_placeholder) == 1 | assert runner_script.count(Python3TemplateTransformer._code_placeholder) == 1 |
| import type { FC } from 'react' | |||||
| import React, { useCallback, useState } from 'react' | |||||
| import { t } from 'i18next' | |||||
| import { | |||||
| RiArrowDownSLine, | |||||
| RiSearchLine, | |||||
| } from '@remixicon/react' | |||||
| import type { CodeDependency } from './types' | |||||
| import { PortalToFollowElem, PortalToFollowElemContent, PortalToFollowElemTrigger } from '@/app/components/base/portal-to-follow-elem' | |||||
| import { Check } from '@/app/components/base/icons/src/vender/line/general' | |||||
| import { XCircle } from '@/app/components/base/icons/src/vender/solid/general' | |||||
| type Props = { | |||||
| value: CodeDependency | |||||
| available_dependencies: CodeDependency[] | |||||
| onChange: (dependency: CodeDependency) => void | |||||
| } | |||||
| const DependencyPicker: FC<Props> = ({ | |||||
| available_dependencies, | |||||
| value, | |||||
| onChange, | |||||
| }) => { | |||||
| const [open, setOpen] = useState(false) | |||||
| const [searchText, setSearchText] = useState('') | |||||
| const handleChange = useCallback((dependency: CodeDependency) => { | |||||
| return () => { | |||||
| setOpen(false) | |||||
| onChange(dependency) | |||||
| } | |||||
| }, [onChange]) | |||||
| return ( | |||||
| <PortalToFollowElem | |||||
| open={open} | |||||
| onOpenChange={setOpen} | |||||
| placement='bottom-start' | |||||
| offset={4} | |||||
| > | |||||
| <PortalToFollowElemTrigger onClick={() => setOpen(!open)} className='flex-grow cursor-pointer'> | |||||
| <div className='flex items-center h-8 justify-between px-2.5 rounded-lg border-0 bg-gray-100 text-gray-900 text-[13px]'> | |||||
| <div className='grow w-0 truncate' title={value.name}>{value.name}</div> | |||||
| <RiArrowDownSLine className='shrink-0 w-3.5 h-3.5 text-gray-700' /> | |||||
| </div> | |||||
| </PortalToFollowElemTrigger> | |||||
| <PortalToFollowElemContent style={{ | |||||
| zIndex: 100, | |||||
| }}> | |||||
| <div className='p-1 bg-white rounded-lg shadow-sm' style={{ | |||||
| width: 350, | |||||
| }}> | |||||
| <div | |||||
| className='shadow-sm bg-white mb-2 mx-1 flex items-center px-2 rounded-lg bg-gray-100' | |||||
| > | |||||
| <RiSearchLine className='shrink-0 ml-[1px] mr-[5px] w-3.5 h-3.5 text-gray-400' /> | |||||
| <input | |||||
| value={searchText} | |||||
| className='grow px-0.5 py-[7px] text-[13px] text-gray-700 bg-transparent appearance-none outline-none caret-primary-600 placeholder:text-gray-400' | |||||
| placeholder={t('workflow.nodes.code.searchDependencies') || ''} | |||||
| onChange={e => setSearchText(e.target.value)} | |||||
| autoFocus | |||||
| /> | |||||
| { | |||||
| searchText && ( | |||||
| <div | |||||
| className='flex items-center justify-center ml-[5px] w-[18px] h-[18px] cursor-pointer' | |||||
| onClick={() => setSearchText('')} | |||||
| > | |||||
| <XCircle className='w-[14px] h-[14px] text-gray-400' /> | |||||
| </div> | |||||
| ) | |||||
| } | |||||
| </div> | |||||
| <div className='max-h-[30vh] overflow-y-auto'> | |||||
| {available_dependencies.filter((v) => { | |||||
| if (!searchText) | |||||
| return true | |||||
| return v.name.toLowerCase().includes(searchText.toLowerCase()) | |||||
| }).map(dependency => ( | |||||
| <div | |||||
| key={dependency.name} | |||||
| className='flex items-center h-[30px] justify-between pl-3 pr-2 rounded-lg hover:bg-gray-100 text-gray-900 text-[13px] cursor-pointer' | |||||
| onClick={handleChange(dependency)} | |||||
| > | |||||
| <div className='w-0 grow truncate'>{dependency.name}</div> | |||||
| {dependency.name === value.name && <Check className='shrink-0 w-4 h-4 text-primary-600' />} | |||||
| </div> | |||||
| ))} | |||||
| </div> | |||||
| </div> | |||||
| </PortalToFollowElemContent> | |||||
| </PortalToFollowElem> | |||||
| ) | |||||
| } | |||||
| export default React.memo(DependencyPicker) |
| import type { FC } from 'react' | |||||
| import React from 'react' | |||||
| import RemoveButton from '../_base/components/remove-button' | |||||
| import type { CodeDependency } from './types' | |||||
| import DependencyPicker from './dependency-picker' | |||||
| type Props = { | |||||
| available_dependencies: CodeDependency[] | |||||
| dependencies: CodeDependency[] | |||||
| handleRemove: (index: number) => void | |||||
| handleChange: (index: number, dependency: CodeDependency) => void | |||||
| } | |||||
| const Dependencies: FC<Props> = ({ | |||||
| available_dependencies, dependencies, handleRemove, handleChange, | |||||
| }) => { | |||||
| return ( | |||||
| <div className='space-y-2'> | |||||
| {dependencies.map((dependency, index) => ( | |||||
| <div className='flex items-center space-x-1' key={index}> | |||||
| <DependencyPicker | |||||
| value={dependency} | |||||
| available_dependencies={available_dependencies} | |||||
| onChange={dependency => handleChange(index, dependency)} | |||||
| /> | |||||
| <RemoveButton | |||||
| className='!p-2 !bg-gray-100 hover:!bg-gray-200' | |||||
| onClick={() => handleRemove(index)} | |||||
| /> | |||||
| </div> | |||||
| ))} | |||||
| </div> | |||||
| ) | |||||
| } | |||||
| export default React.memo(Dependencies) |
| import useConfig from './use-config' | import useConfig from './use-config' | ||||
| import type { CodeNodeType } from './types' | import type { CodeNodeType } from './types' | ||||
| import { CodeLanguage } from './types' | import { CodeLanguage } from './types' | ||||
| import Dependencies from './dependency' | |||||
| import VarList from '@/app/components/workflow/nodes/_base/components/variable/var-list' | import VarList from '@/app/components/workflow/nodes/_base/components/variable/var-list' | ||||
| import OutputVarList from '@/app/components/workflow/nodes/_base/components/variable/output-var-list' | import OutputVarList from '@/app/components/workflow/nodes/_base/components/variable/output-var-list' | ||||
| import AddButton from '@/app/components/base/button/add-button' | import AddButton from '@/app/components/base/button/add-button' | ||||
| varInputs, | varInputs, | ||||
| inputVarValues, | inputVarValues, | ||||
| setInputVarValues, | setInputVarValues, | ||||
| allowDependencies, | |||||
| availableDependencies, | |||||
| handleAddDependency, | |||||
| handleRemoveDependency, | |||||
| handleChangeDependency, | |||||
| } = useConfig(id, data) | } = useConfig(id, data) | ||||
| return ( | return ( | ||||
| filterVar={filterVar} | filterVar={filterVar} | ||||
| /> | /> | ||||
| </Field> | </Field> | ||||
| { | |||||
| allowDependencies | |||||
| ? ( | |||||
| <div> | |||||
| <Split /> | |||||
| <div className='pt-4'> | |||||
| <Field | |||||
| title={t(`${i18nPrefix}.advancedDependencies`)} | |||||
| operations={ | |||||
| <AddButton onClick={() => handleAddDependency({ name: '', version: '' })} /> | |||||
| } | |||||
| tooltip={t(`${i18nPrefix}.advancedDependenciesTip`)!} | |||||
| > | |||||
| <Dependencies | |||||
| available_dependencies={availableDependencies} | |||||
| dependencies={inputs.dependencies || []} | |||||
| handleRemove={index => handleRemoveDependency(index)} | |||||
| handleChange={(index, dependency) => handleChangeDependency(index, dependency)} | |||||
| /> | |||||
| </Field> | |||||
| </div> | |||||
| </div> | |||||
| ) | |||||
| : null | |||||
| } | |||||
| <Split /> | <Split /> | ||||
| <CodeEditor | <CodeEditor | ||||
| isInNode | isInNode |
| code_language: CodeLanguage | code_language: CodeLanguage | ||||
| code: string | code: string | ||||
| outputs: OutputVar | outputs: OutputVar | ||||
| dependencies?: CodeDependency[] | |||||
| } | |||||
| export type CodeDependency = { | |||||
| name: string | |||||
| version: string | |||||
| } | } |
| import { BlockEnum, VarType } from '../../types' | import { BlockEnum, VarType } from '../../types' | ||||
| import type { Var } from '../../types' | import type { Var } from '../../types' | ||||
| import { useStore } from '../../store' | import { useStore } from '../../store' | ||||
| import type { CodeDependency, CodeNodeType, OutputVar } from './types' | |||||
| import type { CodeNodeType, OutputVar } from './types' | |||||
| import { CodeLanguage } from './types' | import { CodeLanguage } from './types' | ||||
| import useNodeCrud from '@/app/components/workflow/nodes/_base/hooks/use-node-crud' | import useNodeCrud from '@/app/components/workflow/nodes/_base/hooks/use-node-crud' | ||||
| import useOneStepRun from '@/app/components/workflow/nodes/_base/hooks/use-one-step-run' | import useOneStepRun from '@/app/components/workflow/nodes/_base/hooks/use-one-step-run' | ||||
| const appId = useAppStore.getState().appDetail?.id | const appId = useAppStore.getState().appDetail?.id | ||||
| const [allLanguageDefault, setAllLanguageDefault] = useState<Record<CodeLanguage, CodeNodeType> | null>(null) | const [allLanguageDefault, setAllLanguageDefault] = useState<Record<CodeLanguage, CodeNodeType> | null>(null) | ||||
| const [allLanguageDependencies, setAllLanguageDependencies] = useState<Record<CodeLanguage, CodeDependency[]> | null>(null) | |||||
| useEffect(() => { | useEffect(() => { | ||||
| if (appId) { | if (appId) { | ||||
| (async () => { | (async () => { | ||||
| const { config: javaScriptConfig } = await fetchNodeDefault(appId, BlockEnum.Code, { code_language: CodeLanguage.javascript }) as any | const { config: javaScriptConfig } = await fetchNodeDefault(appId, BlockEnum.Code, { code_language: CodeLanguage.javascript }) as any | ||||
| const { config: pythonConfig, available_dependencies: pythonDependencies } = await fetchNodeDefault(appId, BlockEnum.Code, { code_language: CodeLanguage.python3 }) as any | |||||
| const { config: pythonConfig } = await fetchNodeDefault(appId, BlockEnum.Code, { code_language: CodeLanguage.python3 }) as any | |||||
| setAllLanguageDefault({ | setAllLanguageDefault({ | ||||
| [CodeLanguage.javascript]: javaScriptConfig as CodeNodeType, | [CodeLanguage.javascript]: javaScriptConfig as CodeNodeType, | ||||
| [CodeLanguage.python3]: pythonConfig as CodeNodeType, | [CodeLanguage.python3]: pythonConfig as CodeNodeType, | ||||
| } as any) | } as any) | ||||
| setAllLanguageDependencies({ | |||||
| [CodeLanguage.python3]: pythonDependencies as CodeDependency[], | |||||
| } as any) | |||||
| })() | })() | ||||
| } | } | ||||
| }, [appId]) | }, [appId]) | ||||
| setInputs, | setInputs, | ||||
| }) | }) | ||||
| const handleAddDependency = useCallback((dependency: CodeDependency) => { | |||||
| const newInputs = produce(inputs, (draft) => { | |||||
| if (!draft.dependencies) | |||||
| draft.dependencies = [] | |||||
| draft.dependencies.push(dependency) | |||||
| }) | |||||
| setInputs(newInputs) | |||||
| }, [inputs, setInputs]) | |||||
| const handleRemoveDependency = useCallback((index: number) => { | |||||
| const newInputs = produce(inputs, (draft) => { | |||||
| if (!draft.dependencies) | |||||
| draft.dependencies = [] | |||||
| draft.dependencies.splice(index, 1) | |||||
| }) | |||||
| setInputs(newInputs) | |||||
| }, [inputs, setInputs]) | |||||
| const handleChangeDependency = useCallback((index: number, dependency: CodeDependency) => { | |||||
| const newInputs = produce(inputs, (draft) => { | |||||
| if (!draft.dependencies) | |||||
| draft.dependencies = [] | |||||
| draft.dependencies[index] = dependency | |||||
| }) | |||||
| setInputs(newInputs) | |||||
| }, [inputs, setInputs]) | |||||
| const [allowDependencies, setAllowDependencies] = useState<boolean>(false) | |||||
| useEffect(() => { | |||||
| if (!inputs.code_language) | |||||
| return | |||||
| if (!allLanguageDependencies) | |||||
| return | |||||
| const newAllowDependencies = !!allLanguageDependencies[inputs.code_language] | |||||
| setAllowDependencies(newAllowDependencies) | |||||
| }, [allLanguageDependencies, inputs.code_language]) | |||||
| const [availableDependencies, setAvailableDependencies] = useState<CodeDependency[]>([]) | |||||
| useEffect(() => { | |||||
| if (!inputs.code_language) | |||||
| return | |||||
| if (!allLanguageDependencies) | |||||
| return | |||||
| const newAvailableDependencies = produce(allLanguageDependencies[inputs.code_language], (draft) => { | |||||
| const currentLanguage = inputs.code_language | |||||
| if (!currentLanguage || !draft || !inputs.dependencies) | |||||
| return [] | |||||
| return draft.filter((dependency) => { | |||||
| return !inputs.dependencies?.find(d => d.name === dependency.name) | |||||
| }) | |||||
| }) | |||||
| setAvailableDependencies(newAvailableDependencies || []) | |||||
| }, [allLanguageDependencies, inputs.code_language, inputs.dependencies]) | |||||
| const [outputKeyOrders, setOutputKeyOrders] = useState<string[]>([]) | const [outputKeyOrders, setOutputKeyOrders] = useState<string[]>([]) | ||||
| const syncOutputKeyOrders = useCallback((outputs: OutputVar) => { | const syncOutputKeyOrders = useCallback((outputs: OutputVar) => { | ||||
| setOutputKeyOrders(Object.keys(outputs)) | setOutputKeyOrders(Object.keys(outputs)) | ||||
| inputVarValues, | inputVarValues, | ||||
| setInputVarValues, | setInputVarValues, | ||||
| runResult, | runResult, | ||||
| availableDependencies, | |||||
| allowDependencies, | |||||
| handleAddDependency, | |||||
| handleRemoveDependency, | |||||
| handleChangeDependency, | |||||
| } | } | ||||
| } | } | ||||