FEAT: NEW WORKFLOW ENGINE (#3160)

Co-authored-by: Joel <iamjoel007@gmail.com>
Co-authored-by: Yeuoly <admin@srmxy.cn>
Co-authored-by: JzoNg <jzongcode@gmail.com>
Co-authored-by: StyleZhang <jasonapring2015@outlook.com>
Co-authored-by: jyong <jyong@dify.ai>
Co-authored-by: nite-knite <nkCoding@gmail.com>
Co-authored-by: jyong <718720800@qq.com>
This commit is contained in:
takatost
2024-04-08 18:51:46 +08:00
committed by GitHub
parent 2fb9850af5
commit 7753ba2d37
1161 changed files with 103836 additions and 10327 deletions

View File

@@ -0,0 +1,81 @@
'use client'
import type { FC } from 'react'
import React, { useState } from 'react'
import cn from 'classnames'
import { useTranslation } from 'react-i18next'
import { Method } from '../types'
import Selector from '../../_base/components/selector'
import useAvailableVarList from '../../_base/hooks/use-available-var-list'
import { VarType } from '../../../types'
import type { Var } from '../../../types'
import Input from '@/app/components/workflow/nodes/_base/components/input-support-select-var'
import { ChevronDown } from '@/app/components/base/icons/src/vender/line/arrows'
const MethodOptions = [
{ label: 'GET', value: Method.get },
{ label: 'POST', value: Method.post },
{ label: 'HEAD', value: Method.head },
{ label: 'PATCH', value: Method.patch },
{ label: 'PUT', value: Method.put },
{ label: 'DELETE', value: Method.delete },
]
type Props = {
nodeId: string
readonly: boolean
method: Method
onMethodChange: (method: Method) => void
url: string
onUrlChange: (url: string) => void
}
const ApiInput: FC<Props> = ({
nodeId,
readonly,
method,
onMethodChange,
url,
onUrlChange,
}) => {
const { t } = useTranslation()
const [isFocus, setIsFocus] = useState(false)
const { availableVars, availableNodes } = useAvailableVarList(nodeId, {
onlyLeafNodeVar: false,
filterVar: (varPayload: Var) => {
return [VarType.string, VarType.number].includes(varPayload.type)
},
})
return (
<div className='flex items-start space-x-1'>
<Selector
value={method}
onChange={onMethodChange}
options={MethodOptions}
trigger={
<div className={cn(readonly && 'cursor-pointer', 'h-8 shrink-0 flex items-center px-2.5 bg-gray-100 border-black/5 rounded-lg')} >
<div className='w-12 pl-0.5 leading-[18px] text-xs font-medium text-gray-900 uppercase'>{method}</div>
{!readonly && <ChevronDown className='ml-1 w-3.5 h-3.5 text-gray-700' />}
</div>
}
popupClassName='top-[34px] w-[108px]'
showChecked
readonly={readonly}
/>
<Input
instanceId='http-api-url'
className={cn(isFocus ? 'shadow-xs bg-gray-50 border-gray-300' : 'bg-gray-100 border-gray-100', 'w-0 grow rounded-lg px-3 py-[6px] border')}
value={url}
onChange={onUrlChange}
readOnly={readonly}
nodesOutputVars={availableVars}
availableNodes={availableNodes}
onFocusChange={setIsFocus}
placeholder={!readonly ? t('workflow.nodes.http.apiPlaceholder')! : ''}
placeholderClassName='!leading-[21px]'
/>
</div >
)
}
export default React.memo(ApiInput)

View File

@@ -0,0 +1,150 @@
'use client'
import type { FC } from 'react'
import { useTranslation } from 'react-i18next'
import React, { useCallback } from 'react'
import produce from 'immer'
import type { Authorization as AuthorizationPayloadType } from '../../types'
import { APIType, AuthorizationType } from '../../types'
import RadioGroup from './radio-group'
import Modal from '@/app/components/base/modal'
import Button from '@/app/components/base/button'
const i18nPrefix = 'workflow.nodes.http.authorization'
type Props = {
payload: AuthorizationPayloadType
onChange: (payload: AuthorizationPayloadType) => void
isShow: boolean
onHide: () => void
}
const Field = ({ title, isRequired, children }: { title: string; isRequired?: boolean; children: JSX.Element }) => {
return (
<div>
<div className='leading-8 text-[13px] font-medium text-gray-700'>
{title}
{isRequired && <span className='ml-0.5 text-[#D92D20]'>*</span>}
</div>
<div>{children}</div>
</div>
)
}
const Authorization: FC<Props> = ({
payload,
onChange,
isShow,
onHide,
}) => {
const { t } = useTranslation()
const [tempPayload, setTempPayload] = React.useState<AuthorizationPayloadType>(payload)
const handleAuthTypeChange = useCallback((type: string) => {
const newPayload = produce(tempPayload, (draft: AuthorizationPayloadType) => {
draft.type = type as AuthorizationType
if (draft.type === AuthorizationType.apiKey && !draft.config) {
draft.config = {
type: APIType.basic,
api_key: '',
}
}
})
setTempPayload(newPayload)
}, [tempPayload, setTempPayload])
const handleAuthAPITypeChange = useCallback((type: string) => {
const newPayload = produce(tempPayload, (draft: AuthorizationPayloadType) => {
if (!draft.config) {
draft.config = {
type: APIType.basic,
api_key: '',
}
}
draft.config.type = type as APIType
})
setTempPayload(newPayload)
}, [tempPayload, setTempPayload])
const handleAPIKeyOrHeaderChange = useCallback((type: 'api_key' | 'header') => {
return (e: React.ChangeEvent<HTMLInputElement>) => {
const newPayload = produce(tempPayload, (draft: AuthorizationPayloadType) => {
if (!draft.config) {
draft.config = {
type: APIType.basic,
api_key: '',
}
}
draft.config[type] = e.target.value
})
setTempPayload(newPayload)
}
}, [tempPayload, setTempPayload])
const handleConfirm = useCallback(() => {
onChange(tempPayload)
onHide()
}, [tempPayload, onChange, onHide])
return (
<Modal
title={t(`${i18nPrefix}.authorization`)}
wrapperClassName='z-50 w-400'
isShow={isShow}
onClose={onHide}
>
<div>
<div className='space-y-2'>
<Field title={t(`${i18nPrefix}.authorizationType`)}>
<RadioGroup
options={[
{ value: AuthorizationType.none, label: t(`${i18nPrefix}.no-auth`) },
{ value: AuthorizationType.apiKey, label: t(`${i18nPrefix}.api-key`) },
]}
value={tempPayload.type}
onChange={handleAuthTypeChange}
/>
</Field>
{tempPayload.type === AuthorizationType.apiKey && (
<>
<Field title={t(`${i18nPrefix}.auth-type`)}>
<RadioGroup
options={[
{ value: APIType.basic, label: t(`${i18nPrefix}.basic`) },
{ value: APIType.bearer, label: t(`${i18nPrefix}.bearer`) },
{ value: APIType.custom, label: t(`${i18nPrefix}.custom`) },
]}
value={tempPayload.config?.type || APIType.basic}
onChange={handleAuthAPITypeChange}
/>
</Field>
{tempPayload.config?.type === APIType.custom && (
<Field title={t(`${i18nPrefix}.header`)} isRequired>
<input
type='text'
className='w-full h-8 leading-8 px-2.5 rounded-lg border-0 bg-gray-100 text-gray-900 text-[13px] placeholder:text-gray-400 focus:outline-none focus:ring-1 focus:ring-inset focus:ring-gray-200'
value={tempPayload.config?.header || ''}
onChange={handleAPIKeyOrHeaderChange('header')}
/>
</Field>
)}
<Field title={t(`${i18nPrefix}.api-key-title`)} isRequired>
<input
type='text'
className='w-full h-8 leading-8 px-2.5 rounded-lg border-0 bg-gray-100 text-gray-900 text-[13px] placeholder:text-gray-400 focus:outline-none focus:ring-1 focus:ring-inset focus:ring-gray-200'
value={tempPayload.config?.api_key || ''}
onChange={handleAPIKeyOrHeaderChange('api_key')}
/>
</Field>
</>
)}
</div>
<div className='mt-6 flex justify-end space-x-2'>
<Button onClick={onHide} className='flex items-center !h-8 leading-[18px] !text-[13px] !font-medium'>{t('common.operation.cancel')}</Button>
<Button type='primary' onClick={handleConfirm} className='flex items-center !h-8 leading-[18px] !text-[13px] !font-medium'>{t('common.operation.save')}</Button>
</div>
</div>
</Modal>
)
}
export default React.memo(Authorization)

View File

@@ -0,0 +1,61 @@
'use client'
import type { FC } from 'react'
import React, { useCallback } from 'react'
import cn from 'classnames'
type Option = {
value: string
label: string
}
type ItemProps = {
title: string
onClick: () => void
isSelected: boolean
}
const Item: FC<ItemProps> = ({
title,
onClick,
isSelected,
}) => {
return (
<div
className={cn(
isSelected ? 'border-[2px] border-primary-400 bg-white shadow-xs' : 'border border-gray-100 bg-gray-25',
'w-0 grow flex items-center justify-center h-8 cursor-pointer rounded-lg text-[13px] font-normal text-gray-900')
}
onClick={onClick}
>
{title}
</div>
)
}
type Props = {
options: Option[]
value: string
onChange: (value: string) => void
}
const RadioGroup: FC<Props> = ({
options,
value,
onChange,
}) => {
const handleChange = useCallback((value: string) => {
return () => onChange(value)
}, [onChange])
return (
<div className='flex space-x-2'>
{options.map(option => (
<Item
key={option.value}
title={option.label}
onClick={handleChange(option.value)}
isSelected={option.value === value}
/>
))}
</div>
)
}
export default React.memo(RadioGroup)

View File

@@ -0,0 +1,157 @@
'use client'
import type { FC } from 'react'
import React, { useCallback, useEffect } from 'react'
import produce from 'immer'
import cn from 'classnames'
import type { Body } from '../../types'
import { BodyType } from '../../types'
import useKeyValueList from '../../hooks/use-key-value-list'
import KeyValue from '../key-value'
import useAvailableVarList from '../../../_base/hooks/use-available-var-list'
import InputWithVar from '@/app/components/workflow/nodes/_base/components/prompt/editor'
import type { Var } from '@/app/components/workflow/types'
import { VarType } from '@/app/components/workflow/types'
type Props = {
readonly: boolean
nodeId: string
payload: Body
onChange: (payload: Body) => void
}
const allTypes = [
BodyType.none,
BodyType.formData,
BodyType.xWwwFormUrlencoded,
BodyType.rawText,
BodyType.json,
]
const bodyTextMap = {
[BodyType.none]: 'none',
[BodyType.formData]: 'form-data',
[BodyType.xWwwFormUrlencoded]: 'x-www-form-urlencoded',
[BodyType.rawText]: 'raw text',
[BodyType.json]: 'JSON',
}
const EditBody: FC<Props> = ({
readonly,
nodeId,
payload,
onChange,
}) => {
const { type } = payload
const { availableVars, availableNodes } = useAvailableVarList(nodeId, {
onlyLeafNodeVar: false,
filterVar: (varPayload: Var) => {
return [VarType.string, VarType.number].includes(varPayload.type)
},
})
const handleTypeChange = useCallback((e: React.ChangeEvent<HTMLInputElement>) => {
const newType = e.target.value as BodyType
onChange({
type: newType,
data: '',
})
// eslint-disable-next-line @typescript-eslint/no-use-before-define
setBody([])
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [onChange])
const {
list: body,
setList: setBody,
addItem: addBody,
} = useKeyValueList(payload.data, (value) => {
const newBody = produce(payload, (draft: Body) => {
draft.data = value
})
onChange(newBody)
}, type === BodyType.json)
const isCurrentKeyValue = type === BodyType.formData || type === BodyType.xWwwFormUrlencoded
useEffect(() => {
if (!isCurrentKeyValue)
return
const newBody = produce(payload, (draft: Body) => {
draft.data = body.map((item) => {
if (!item.key && !item.value)
return ''
return `${item.key}:${item.value}`
}).join('\n')
})
onChange(newBody)
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [isCurrentKeyValue])
const handleBodyValueChange = useCallback((value: string) => {
const newBody = produce(payload, (draft: Body) => {
draft.data = value
})
onChange(newBody)
}, [onChange, payload])
return (
<div>
{/* body type */}
<div className='flex flex-wrap'>
{allTypes.map(t => (
<label key={t} htmlFor={`body-type-${t}`} className='mr-4 flex items-center h-7 space-x-2'>
<input
type="radio"
id={`body-type-${t}`}
value={t}
checked={type === t}
onChange={handleTypeChange}
disabled={readonly}
/>
<div className='leading-[18px] text-[13px] font-normal text-gray-700'>{bodyTextMap[t]}</div>
</label>
))}
</div>
{/* body value */}
<div className={cn(type !== BodyType.none && 'mt-1')}>
{type === BodyType.none && null}
{(type === BodyType.formData || type === BodyType.xWwwFormUrlencoded) && (
<KeyValue
readonly={readonly}
nodeId={nodeId}
list={body}
onChange={setBody}
onAdd={addBody}
/>
)}
{type === BodyType.rawText && (
<InputWithVar
instanceId={'http-body-raw'}
title={<div className='uppercase'>Raw text</div>}
onChange={handleBodyValueChange}
value={payload.data}
justVar
nodesOutputVars={availableVars}
availableNodes={availableNodes}
readOnly={readonly}
/>
)}
{type === BodyType.json && (
<InputWithVar
instanceId={'http-body-json'}
title='JSON'
value={payload.data}
onChange={handleBodyValueChange}
justVar
nodesOutputVars={availableVars}
availableNodes={availableNodes}
readOnly={readonly}
/>
)}
</div>
</div>
)
}
export default React.memo(EditBody)

View File

@@ -0,0 +1,61 @@
'use client'
import type { FC } from 'react'
import React, { useCallback } from 'react'
import { useTranslation } from 'react-i18next'
import TextEditor from '@/app/components/workflow/nodes/_base/components/editor/text-editor'
import { LayoutGrid02 } from '@/app/components/base/icons/src/vender/line/layout'
const i18nPrefix = 'workflow.nodes.http'
type Props = {
value: string
onChange: (value: string) => void
onSwitchToKeyValueEdit: () => void
}
const BulkEdit: FC<Props> = ({
value,
onChange,
onSwitchToKeyValueEdit,
}) => {
const { t } = useTranslation()
const [tempValue, setTempValue] = React.useState(value)
const handleChange = useCallback((value: string) => {
setTempValue(value)
}, [])
const handleBlur = useCallback(() => {
onChange(tempValue)
}, [tempValue, onChange])
const handleSwitchToKeyValueEdit = useCallback(() => {
onChange(tempValue)
onSwitchToKeyValueEdit()
}, [tempValue, onChange, onSwitchToKeyValueEdit])
return (
<div>
<TextEditor
title={<div className='uppercase'>{t(`${i18nPrefix}.bulkEdit`)}</div>}
value={tempValue}
onChange={handleChange}
onBlur={handleBlur}
headerRight={
<div className='flex items-center h-[18px]'>
<div
className='flex items-center space-x-1 cursor-pointer'
onClick={handleSwitchToKeyValueEdit}
>
<LayoutGrid02 className='w-3 h-3 text-gray-500' />
<div className='leading-[18px] text-xs font-normal text-gray-500'>{t(`${i18nPrefix}.keyValueEdit`)}</div>
</div>
<div className='ml-3 mr-1.5 w-px h-3 bg-gray-200'></div>
</div>
}
minHeight={150}
/>
</div>
)
}
export default React.memo(BulkEdit)

View File

@@ -0,0 +1,59 @@
'use client'
import type { FC } from 'react'
import React from 'react'
import type { KeyValue } from '../../types'
import KeyValueEdit from './key-value-edit'
type Props = {
readonly: boolean
nodeId: string
list: KeyValue[]
onChange: (newList: KeyValue[]) => void
onAdd: () => void
// toggleKeyValueEdit: () => void
}
const KeyValueList: FC<Props> = ({
readonly,
nodeId,
list,
onChange,
onAdd,
// toggleKeyValueEdit,
}) => {
// const handleBulkValueChange = useCallback((value: string) => {
// const newList = value.split('\n').map((item) => {
// const [key, value] = item.split(':')
// return {
// key: key ? key.trim() : '',
// value: value ? value.trim() : '',
// }
// })
// onChange(newList)
// }, [onChange])
// const bulkList = (() => {
// const res = list.map((item) => {
// if (!item.key && !item.value)
// return ''
// if (!item.value)
// return item.key
// return `${item.key}:${item.value}`
// }).join('\n')
// return res
// })()
return <KeyValueEdit
readonly={readonly}
nodeId={nodeId}
list={list}
onChange={onChange}
onAdd={onAdd}
// onSwitchToBulkEdit={toggleKeyValueEdit}
/>
// : <BulkEdit
// value={bulkList}
// onChange={handleBulkValueChange}
// onSwitchToKeyValueEdit={toggleKeyValueEdit}
// />
}
export default React.memo(KeyValueList)

View File

@@ -0,0 +1,88 @@
'use client'
import type { FC } from 'react'
import React, { useCallback } from 'react'
import produce from 'immer'
import { useTranslation } from 'react-i18next'
import type { KeyValue } from '../../../types'
import KeyValueItem from './item'
// import TooltipPlus from '@/app/components/base/tooltip-plus'
// import { EditList } from '@/app/components/base/icons/src/vender/solid/communication'
const i18nPrefix = 'workflow.nodes.http'
type Props = {
readonly: boolean
nodeId: string
list: KeyValue[]
onChange: (newList: KeyValue[]) => void
onAdd: () => void
// onSwitchToBulkEdit: () => void
}
const KeyValueList: FC<Props> = ({
readonly,
nodeId,
list,
onChange,
onAdd,
// onSwitchToBulkEdit,
}) => {
const { t } = useTranslation()
const handleChange = useCallback((index: number) => {
return (newItem: KeyValue) => {
const newList = produce(list, (draft: any) => {
draft[index] = newItem
})
onChange(newList)
}
}, [list, onChange])
const handleRemove = useCallback((index: number) => {
return () => {
const newList = produce(list, (draft: any) => {
draft.splice(index, 1)
})
onChange(newList)
}
}, [list, onChange])
return (
<div className='border border-gray-200 rounded-lg overflow-hidden'>
<div className='flex items-center h-7 leading-7 text-xs font-medium text-gray-500 uppercase'>
<div className='w-1/2 h-full pl-3 border-r border-gray-200'>{t(`${i18nPrefix}.key`)}</div>
<div className='flex w-1/2 h-full pl-3 pr-1 items-center justify-between'>
<div>{t(`${i18nPrefix}.value`)}</div>
{/* {!readonly && (
<TooltipPlus
popupContent={t(`${i18nPrefix}.bulkEdit`)}
>
<div
className='p-1 cursor-pointer rounded-md hover:bg-black/5 text-gray-500 hover:text-gray-800'
onClick={onSwitchToBulkEdit}
>
<EditList className='w-3 h-3' />
</div>
</TooltipPlus>)} */}
</div>
</div>
{
list.map((item, index) => (
<KeyValueItem
key={item.id}
instanceId={item.id!}
nodeId={nodeId}
payload={item}
onChange={handleChange(index)}
onRemove={handleRemove(index)}
isLastItem={index === list.length - 1}
onAdd={onAdd}
readonly={readonly}
canRemove={list.length > 1}
/>
))
}
</div>
)
}
export default React.memo(KeyValueList)

View File

@@ -0,0 +1,97 @@
'use client'
import type { FC } from 'react'
import React, { useCallback, useState } from 'react'
import cn from 'classnames'
import { useTranslation } from 'react-i18next'
import useAvailableVarList from '../../../../_base/hooks/use-available-var-list'
import RemoveButton from '@/app/components/workflow/nodes/_base/components/remove-button'
import Input from '@/app/components/workflow/nodes/_base/components/input-support-select-var'
import type { Var } from '@/app/components/workflow/types'
import { VarType } from '@/app/components/workflow/types'
type Props = {
className?: string
instanceId?: string
nodeId: string
value: string
onChange: (newValue: string) => void
hasRemove: boolean
onRemove?: () => void
placeholder?: string
readOnly?: boolean
}
const InputItem: FC<Props> = ({
className,
instanceId,
nodeId,
value,
onChange,
hasRemove,
onRemove,
placeholder,
readOnly,
}) => {
const { t } = useTranslation()
const hasValue = !!value
const [isFocus, setIsFocus] = useState(false)
const { availableVars, availableNodes } = useAvailableVarList(nodeId, {
onlyLeafNodeVar: false,
filterVar: (varPayload: Var) => {
return [VarType.string, VarType.number].includes(varPayload.type)
},
})
const handleRemove = useCallback((e: React.MouseEvent) => {
e.stopPropagation()
onRemove?.()
}, [onRemove])
return (
<div className={cn(className, 'hover:bg-gray-50 hover:cursor-text', 'relative flex h-full items-center')}>
{(!readOnly)
? (
<Input
instanceId={instanceId}
className={cn(isFocus ? 'bg-gray-100' : 'bg-width', 'w-0 grow px-3 py-1')}
value={value}
onChange={onChange}
readOnly={readOnly}
nodesOutputVars={availableVars}
availableNodes={availableNodes}
onFocusChange={setIsFocus}
placeholder={t('workflow.nodes.http.insertVarPlaceholder')!}
placeholderClassName='!leading-[21px]'
/>
)
: <div
className="pl-0.5 w-full h-[18px] leading-[18px]"
>
{!hasValue && <div className='text-gray-300 text-xs font-normal'>{placeholder}</div>}
{hasValue && (
<Input
instanceId={instanceId}
className={cn(isFocus ? 'shadow-xs bg-gray-50 border-gray-300' : 'bg-gray-100 border-gray-100', 'w-0 grow rounded-lg px-3 py-[6px] border')}
value={value}
onChange={onChange}
readOnly={readOnly}
nodesOutputVars={availableVars}
availableNodes={availableNodes}
onFocusChange={setIsFocus}
placeholder={t('workflow.nodes.http.insertVarPlaceholder')!}
placeholderClassName='!leading-[21px]'
/>
)}
</div>}
{hasRemove && !isFocus && (
<RemoveButton
className='group-hover:block hidden absolute right-1 top-0.5'
onClick={handleRemove}
/>
)}
</div>
)
}
export default React.memo(InputItem)

View File

@@ -0,0 +1,79 @@
'use client'
import type { FC } from 'react'
import React, { useCallback } from 'react'
import { useTranslation } from 'react-i18next'
import cn from 'classnames'
import produce from 'immer'
import type { KeyValue } from '../../../types'
import InputItem from './input-item'
const i18nPrefix = 'workflow.nodes.http'
type Props = {
instanceId: string
className?: string
nodeId: string
readonly: boolean
canRemove: boolean
payload: KeyValue
onChange: (newPayload: KeyValue) => void
onRemove: () => void
isLastItem: boolean
onAdd: () => void
}
const KeyValueItem: FC<Props> = ({
instanceId,
className,
nodeId,
readonly,
canRemove,
payload,
onChange,
onRemove,
isLastItem,
onAdd,
}) => {
const { t } = useTranslation()
const handleChange = useCallback((key: string) => {
return (value: string) => {
const newPayload = produce(payload, (draft: any) => {
draft[key] = value
})
onChange(newPayload)
if (key === 'value' && isLastItem)
onAdd()
}
}, [onChange, onAdd, isLastItem, payload])
return (
// group class name is for hover row show remove button
<div className={cn(className, 'group flex items-start h-min-7 border-t border-gray-200')}>
<div className='w-1/2 h-full border-r border-gray-200'>
<InputItem
instanceId={`http-key-${instanceId}`}
nodeId={nodeId}
value={payload.key}
onChange={handleChange('key')}
hasRemove={false}
placeholder={t(`${i18nPrefix}.key`)!}
readOnly={readonly}
/>
</div>
<div className='w-1/2 h-full'>
<InputItem
instanceId={`http-value-${instanceId}`}
nodeId={nodeId}
value={payload.value}
onChange={handleChange('value')}
hasRemove={!readonly && canRemove}
onRemove={onRemove}
placeholder={t(`${i18nPrefix}.value`)!}
readOnly={readonly}
/>
</div>
</div>
)
}
export default React.memo(KeyValueItem)

View File

@@ -0,0 +1,48 @@
import { BlockEnum } from '../../types'
import type { NodeDefault } from '../../types'
import { AuthorizationType, BodyType, type HttpNodeType, Method } from './types'
import { ALL_CHAT_AVAILABLE_BLOCKS, ALL_COMPLETION_AVAILABLE_BLOCKS } from '@/app/components/workflow/constants'
const nodeDefault: NodeDefault<HttpNodeType> = {
defaultValue: {
variables: [],
method: Method.get,
url: '',
authorization: {
type: AuthorizationType.none,
config: null,
},
headers: '',
params: '',
body: {
type: BodyType.none,
data: '',
},
},
getAvailablePrevNodes(isChatMode: boolean) {
const nodes = isChatMode
? ALL_CHAT_AVAILABLE_BLOCKS
: ALL_COMPLETION_AVAILABLE_BLOCKS.filter(type => type !== BlockEnum.End)
return nodes
},
getAvailableNextNodes(isChatMode: boolean) {
const nodes = isChatMode ? ALL_CHAT_AVAILABLE_BLOCKS : ALL_COMPLETION_AVAILABLE_BLOCKS
return nodes
},
checkValid(payload: HttpNodeType, t: any) {
let errorMessages = ''
if (!errorMessages && !payload.url)
errorMessages = t('workflow.errorMsg.fieldRequired', { field: t('workflow.nodes.http.api') })
if (!errorMessages && !payload.url.startsWith('http://') && !payload.url.startsWith('https://'))
errorMessages = t('workflow.nodes.http.notStartWithHttp')
return {
isValid: !errorMessages,
errorMessage: errorMessages,
}
},
}
export default nodeDefault

View File

@@ -0,0 +1,58 @@
import { useCallback, useEffect, useState } from 'react'
import { useBoolean } from 'ahooks'
import { uniqueId } from 'lodash'
import type { KeyValue } from '../types'
const UNIQUE_ID_PREFIX = 'key-value-'
const strToKeyValueList = (value: string) => {
return value.split('\n').map((item) => {
const [key, value] = item.split(':')
return {
id: uniqueId(UNIQUE_ID_PREFIX),
key: key.trim(),
value: value?.trim(),
}
})
}
const useKeyValueList = (value: string, onChange: (value: string) => void, noFilter?: boolean) => {
const [list, doSetList] = useState<KeyValue[]>(value ? strToKeyValueList(value) : [])
const setList = (l: KeyValue[]) => {
doSetList(l.map((item) => {
return {
...item,
id: item.id || uniqueId(UNIQUE_ID_PREFIX),
}
}))
}
useEffect(() => {
if (noFilter)
return
const newValue = list.filter(item => item.key && item.value).map(item => `${item.key}:${item.value}`).join('\n')
if (newValue !== value)
onChange(newValue)
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [list, noFilter])
const addItem = useCallback(() => {
setList([...list, {
id: uniqueId(UNIQUE_ID_PREFIX),
key: '',
value: '',
}])
}, [list])
const [isKeyValueEdit, {
toggle: toggleIsKeyValueEdit,
}] = useBoolean(true)
return {
list: list.length === 0 ? [{ id: uniqueId(UNIQUE_ID_PREFIX), key: '', value: '' }] : list, // no item can not add new item
setList,
addItem,
isKeyValueEdit,
toggleIsKeyValueEdit,
}
}
export default useKeyValueList

View File

@@ -0,0 +1,29 @@
import type { FC } from 'react'
import React from 'react'
import ReadonlyInputWithSelectVar from '../_base/components/readonly-input-with-select-var'
import type { HttpNodeType } from './types'
import type { NodeProps } from '@/app/components/workflow/types'
const Node: FC<NodeProps<HttpNodeType>> = ({
id,
data,
}) => {
const { method, url } = data
if (!url)
return null
return (
<div className='mb-1 px-3 py-1'>
<div className='flex items-start p-1 rounded-md bg-gray-100'>
<div className='flex items-center h-4 shrink-0 px-1 rounded bg-gray-25 text-xs font-semibold text-gray-700 uppercase'>{method}</div>
<div className='pl-1'>
<ReadonlyInputWithSelectVar
value={url}
nodeId={id}
/>
</div>
</div>
</div>
)
}
export default React.memo(Node)

View File

@@ -0,0 +1,171 @@
import type { FC } from 'react'
import React from 'react'
import { useTranslation } from 'react-i18next'
import cn from 'classnames'
import useConfig from './use-config'
import ApiInput from './components/api-input'
import KeyValue from './components/key-value'
import EditBody from './components/edit-body'
import AuthorizationModal from './components/authorization'
import type { HttpNodeType } from './types'
import Field from '@/app/components/workflow/nodes/_base/components/field'
import Split from '@/app/components/workflow/nodes/_base/components/split'
import OutputVars, { VarItem } from '@/app/components/workflow/nodes/_base/components/output-vars'
import { Settings01 } from '@/app/components/base/icons/src/vender/line/general'
import type { NodePanelProps } from '@/app/components/workflow/types'
import BeforeRunForm from '@/app/components/workflow/nodes/_base/components/before-run-form'
import ResultPanel from '@/app/components/workflow/run/result-panel'
const i18nPrefix = 'workflow.nodes.http'
const Panel: FC<NodePanelProps<HttpNodeType>> = ({
id,
data,
}) => {
const { t } = useTranslation()
const {
readOnly,
inputs,
handleMethodChange,
handleUrlChange,
headers,
setHeaders,
addHeader,
params,
setParams,
addParam,
setBody,
isShowAuthorization,
showAuthorization,
hideAuthorization,
setAuthorization,
// single run
isShowSingleRun,
hideSingleRun,
runningStatus,
handleRun,
handleStop,
varInputs,
inputVarValues,
setInputVarValues,
runResult,
} = useConfig(id, data)
return (
<div className='mt-2'>
<div className='px-4 pb-4 space-y-4'>
<Field
title={t(`${i18nPrefix}.api`)}
operations={
<div
onClick={showAuthorization}
className={cn(!readOnly && 'cursor-pointer hover:bg-gray-50', 'flex items-center h-6 space-x-1 px-2 rounded-md ')}
>
{!readOnly && <Settings01 className='w-3 h-3 text-gray-500' />}
<div className='text-xs font-medium text-gray-500'>
{t(`${i18nPrefix}.authorization.authorization`)}
<span className='ml-1 text-gray-700'>{t(`${i18nPrefix}.authorization.${inputs.authorization.type}`)}</span>
</div>
</div>
}
>
<ApiInput
nodeId={id}
readonly={readOnly}
method={inputs.method}
onMethodChange={handleMethodChange}
url={inputs.url}
onUrlChange={handleUrlChange}
/>
</Field>
<Field
title={t(`${i18nPrefix}.headers`)}
>
<KeyValue
nodeId={id}
list={headers}
onChange={setHeaders}
onAdd={addHeader}
readonly={readOnly}
/>
</Field>
<Field
title={t(`${i18nPrefix}.params`)}
>
<KeyValue
nodeId={id}
list={params}
onChange={setParams}
onAdd={addParam}
readonly={readOnly}
/>
</Field>
<Field
title={t(`${i18nPrefix}.body`)}
>
<EditBody
nodeId={id}
readonly={readOnly}
payload={inputs.body}
onChange={setBody}
/>
</Field>
</div>
{(isShowAuthorization && !readOnly) && (
<AuthorizationModal
isShow
onHide={hideAuthorization}
payload={inputs.authorization}
onChange={setAuthorization}
/>
)}
<Split />
<div className='px-4 pt-4 pb-2'>
<OutputVars>
<>
<VarItem
name='body'
type='string'
description={t(`${i18nPrefix}.outputVars.body`)}
/>
<VarItem
name='status_code'
type='number'
description={t(`${i18nPrefix}.outputVars.statusCode`)}
/>
<VarItem
name='headers'
type='object'
description={t(`${i18nPrefix}.outputVars.headers`)}
/>
<VarItem
name='files'
type='Array[File]'
description={t(`${i18nPrefix}.outputVars.files`)}
/>
</>
</OutputVars>
</div>
{isShowSingleRun && (
<BeforeRunForm
nodeName={inputs.title}
onHide={hideSingleRun}
forms={[
{
inputs: varInputs,
values: inputVarValues,
onChange: setInputVarValues,
},
]}
runningStatus={runningStatus}
onRun={handleRun}
onStop={handleStop}
result={<ResultPanel {...runResult} showSteps={false} />}
/>
)}
</div >
)
}
export default React.memo(Panel)

View File

@@ -0,0 +1,59 @@
import type { CommonNodeType, Variable } from '@/app/components/workflow/types'
export enum Method {
get = 'get',
post = 'post',
head = 'head',
patch = 'patch',
put = 'put',
delete = 'delete',
}
export enum BodyType {
none = 'none',
formData = 'form-data',
xWwwFormUrlencoded = 'x-www-form-urlencoded',
rawText = 'raw-text',
json = 'json',
}
export type KeyValue = {
id?: string
key: string
value: string
}
export type Body = {
type: BodyType
data: string
}
export enum AuthorizationType {
none = 'no-auth',
apiKey = 'api-key',
}
export enum APIType {
basic = 'basic',
bearer = 'bearer',
custom = 'custom',
}
export type Authorization = {
type: AuthorizationType
config?: {
type: APIType
api_key: string
header?: string
} | null
}
export type HttpNodeType = CommonNodeType & {
variables: Variable[]
method: Method
url: string
headers: string
params: string
body: Body
authorization: Authorization
}

View File

@@ -0,0 +1,164 @@
import { useCallback } from 'react'
import produce from 'immer'
import { useBoolean } from 'ahooks'
import useVarList from '../_base/hooks/use-var-list'
import { VarType } from '../../types'
import type { Var } from '../../types'
import type { Authorization, Body, HttpNodeType, Method } from './types'
import useKeyValueList from './hooks/use-key-value-list'
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 {
useNodesReadOnly,
} from '@/app/components/workflow/hooks'
const useConfig = (id: string, payload: HttpNodeType) => {
const { nodesReadOnly: readOnly } = useNodesReadOnly()
const { inputs, setInputs } = useNodeCrud<HttpNodeType>(id, payload)
const { handleVarListChange, handleAddVariable } = useVarList<HttpNodeType>({
inputs,
setInputs,
})
const handleMethodChange = useCallback((method: Method) => {
const newInputs = produce(inputs, (draft: HttpNodeType) => {
draft.method = method
})
setInputs(newInputs)
}, [inputs, setInputs])
const handleUrlChange = useCallback((url: string) => {
const newInputs = produce(inputs, (draft: HttpNodeType) => {
draft.url = url
})
setInputs(newInputs)
}, [inputs, setInputs])
const handleFieldChange = useCallback((field: string) => {
return (value: string) => {
const newInputs = produce(inputs, (draft: HttpNodeType) => {
(draft as any)[field] = value
})
setInputs(newInputs)
}
}, [inputs, setInputs])
const {
list: headers,
setList: setHeaders,
addItem: addHeader,
isKeyValueEdit: isHeaderKeyValueEdit,
toggleIsKeyValueEdit: toggleIsHeaderKeyValueEdit,
} = useKeyValueList(inputs.headers, handleFieldChange('headers'))
const {
list: params,
setList: setParams,
addItem: addParam,
isKeyValueEdit: isParamKeyValueEdit,
toggleIsKeyValueEdit: toggleIsParamKeyValueEdit,
} = useKeyValueList(inputs.params, handleFieldChange('params'))
const setBody = useCallback((data: Body) => {
const newInputs = produce(inputs, (draft: HttpNodeType) => {
draft.body = data
})
setInputs(newInputs)
}, [inputs, setInputs])
// authorization
const [isShowAuthorization, {
setTrue: showAuthorization,
setFalse: hideAuthorization,
}] = useBoolean(false)
const setAuthorization = useCallback((authorization: Authorization) => {
const newInputs = produce(inputs, (draft: HttpNodeType) => {
draft.authorization = authorization
})
setInputs(newInputs)
}, [inputs, setInputs])
const filterVar = useCallback((varPayload: Var) => {
return [VarType.string, VarType.number].includes(varPayload.type)
}, [])
// single run
const {
isShowSingleRun,
hideSingleRun,
getInputVars,
runningStatus,
handleRun,
handleStop,
runInputData,
setRunInputData,
runResult,
} = useOneStepRun<HttpNodeType>({
id,
data: inputs,
defaultRunInputData: {},
})
const varInputs = getInputVars([
inputs.url,
inputs.headers,
inputs.params,
inputs.body.data,
])
const inputVarValues = (() => {
const vars: Record<string, any> = {}
Object.keys(runInputData)
.forEach((key) => {
vars[key] = runInputData[key]
})
return vars
})()
const setInputVarValues = useCallback((newPayload: Record<string, any>) => {
setRunInputData(newPayload)
}, [setRunInputData])
return {
readOnly,
inputs,
handleVarListChange,
handleAddVariable,
filterVar,
handleMethodChange,
handleUrlChange,
// headers
headers,
setHeaders,
addHeader,
isHeaderKeyValueEdit,
toggleIsHeaderKeyValueEdit,
// params
params,
setParams,
addParam,
isParamKeyValueEdit,
toggleIsParamKeyValueEdit,
// body
setBody,
// authorization
isShowAuthorization,
showAuthorization,
hideAuthorization,
setAuthorization,
// single run
isShowSingleRun,
hideSingleRun,
runningStatus,
handleRun,
handleStop,
varInputs,
inputVarValues,
setInputVarValues,
runResult,
}
}
export default useConfig

View File

@@ -0,0 +1,5 @@
import type { HttpNodeType } from './types'
export const checkNodeValid = (payload: HttpNodeType) => {
return true
}