mirror of
https://github.com/langgenius/dify.git
synced 2024-11-16 11:42:29 +08:00
Merge 2bff917f2b
into d05fee1182
This commit is contained in:
commit
98452d6af2
154
web/app/components/workflow/nodes/http/components/curl-panel.tsx
Normal file
154
web/app/components/workflow/nodes/http/components/curl-panel.tsx
Normal file
|
@ -0,0 +1,154 @@
|
||||||
|
'use client'
|
||||||
|
import type { FC } from 'react'
|
||||||
|
import React, { useCallback, useState } from 'react'
|
||||||
|
import { useTranslation } from 'react-i18next'
|
||||||
|
import { BodyType, type HttpNodeType, Method } from '../types'
|
||||||
|
import Modal from '@/app/components/base/modal'
|
||||||
|
import Button from '@/app/components/base/button'
|
||||||
|
import Toast from '@/app/components/base/toast'
|
||||||
|
import { useNodesInteractions } from '@/app/components/workflow/hooks'
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
nodeId: string
|
||||||
|
isShow: boolean
|
||||||
|
onHide: () => void
|
||||||
|
handleCurlImport: (node: HttpNodeType) => void
|
||||||
|
}
|
||||||
|
|
||||||
|
const parseCurl = (curlCommand: string): { node: HttpNodeType | null; error: string | null } => {
|
||||||
|
if (!curlCommand.trim().toLowerCase().startsWith('curl'))
|
||||||
|
return { node: null, error: 'Invalid cURL command. Command must start with "curl".' }
|
||||||
|
|
||||||
|
const node: Partial<HttpNodeType> = {
|
||||||
|
title: 'HTTP Request',
|
||||||
|
desc: 'Imported from cURL',
|
||||||
|
method: Method.get,
|
||||||
|
url: '',
|
||||||
|
headers: '',
|
||||||
|
params: '',
|
||||||
|
body: { type: BodyType.none, data: '' },
|
||||||
|
}
|
||||||
|
const args = curlCommand.match(/(?:[^\s"']+|"[^"]*"|'[^']*')+/g) || []
|
||||||
|
|
||||||
|
for (let i = 1; i < args.length; i++) {
|
||||||
|
const arg = args[i].replace(/^['"]|['"]$/g, '')
|
||||||
|
switch (arg) {
|
||||||
|
case '-X':
|
||||||
|
case '--request':
|
||||||
|
if (i + 1 >= args.length)
|
||||||
|
return { node: null, error: 'Missing HTTP method after -X or --request.' }
|
||||||
|
node.method = (args[++i].replace(/^['"]|['"]$/g, '') as Method) || Method.get
|
||||||
|
break
|
||||||
|
case '-H':
|
||||||
|
case '--header':
|
||||||
|
if (i + 1 >= args.length)
|
||||||
|
return { node: null, error: 'Missing header value after -H or --header.' }
|
||||||
|
node.headers += (node.headers ? '\n' : '') + args[++i].replace(/^['"]|['"]$/g, '')
|
||||||
|
break
|
||||||
|
case '-d':
|
||||||
|
case '--data':
|
||||||
|
case '--data-raw':
|
||||||
|
case '--data-binary':
|
||||||
|
if (i + 1 >= args.length)
|
||||||
|
return { node: null, error: 'Missing data value after -d, --data, --data-raw, or --data-binary.' }
|
||||||
|
node.body = { type: BodyType.rawText, data: args[++i].replace(/^['"]|['"]$/g, '') }
|
||||||
|
break
|
||||||
|
case '-F':
|
||||||
|
case '--form': {
|
||||||
|
if (i + 1 >= args.length)
|
||||||
|
return { node: null, error: 'Missing form data after -F or --form.' }
|
||||||
|
if (node.body?.type !== BodyType.formData)
|
||||||
|
node.body = { type: BodyType.formData, data: '' }
|
||||||
|
const formData = args[++i].replace(/^['"]|['"]$/g, '')
|
||||||
|
const [key, ...valueParts] = formData.split('=')
|
||||||
|
if (!key)
|
||||||
|
return { node: null, error: 'Invalid form data format.' }
|
||||||
|
let value = valueParts.join('=')
|
||||||
|
|
||||||
|
// To support command like `curl -F "file=@/path/to/file;type=application/zip"`
|
||||||
|
// the `;type=application/zip` should translate to `Content-Type: application/zip`
|
||||||
|
const typeMatch = value.match(/^(.+?);type=(.+)$/)
|
||||||
|
if (typeMatch) {
|
||||||
|
const [, actualValue, mimeType] = typeMatch
|
||||||
|
value = actualValue
|
||||||
|
node.headers += `${node.headers ? '\n' : ''}Content-Type: ${mimeType}`
|
||||||
|
}
|
||||||
|
|
||||||
|
node.body.data += `${node.body.data ? '\n' : ''}${key}:${value}`
|
||||||
|
break
|
||||||
|
}
|
||||||
|
case '--json':
|
||||||
|
if (i + 1 >= args.length)
|
||||||
|
return { node: null, error: 'Missing JSON data after --json.' }
|
||||||
|
node.body = { type: BodyType.json, data: args[++i].replace(/^['"]|['"]$/g, '') }
|
||||||
|
break
|
||||||
|
default:
|
||||||
|
if (arg.startsWith('http') && !node.url)
|
||||||
|
node.url = arg
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!node.url)
|
||||||
|
return { node: null, error: 'Missing URL or url not start with http.' }
|
||||||
|
|
||||||
|
// Extract query params from URL
|
||||||
|
const urlParts = node.url?.split('?') || []
|
||||||
|
if (urlParts.length > 1) {
|
||||||
|
node.url = urlParts[0]
|
||||||
|
node.params = urlParts[1].replace(/&/g, '\n').replace(/=/g, ': ')
|
||||||
|
}
|
||||||
|
|
||||||
|
return { node: node as HttpNodeType, error: null }
|
||||||
|
}
|
||||||
|
|
||||||
|
const CurlPanel: FC<Props> = ({ nodeId, isShow, onHide, handleCurlImport }) => {
|
||||||
|
const [inputString, setInputString] = useState('')
|
||||||
|
const { handleNodeSelect } = useNodesInteractions()
|
||||||
|
const { t } = useTranslation()
|
||||||
|
|
||||||
|
const handleSave = useCallback(() => {
|
||||||
|
const { node, error } = parseCurl(inputString)
|
||||||
|
if (error) {
|
||||||
|
Toast.notify({
|
||||||
|
type: 'error',
|
||||||
|
message: error,
|
||||||
|
})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if (!node)
|
||||||
|
return
|
||||||
|
|
||||||
|
onHide()
|
||||||
|
handleCurlImport(node)
|
||||||
|
// Close the panel then open it again to make the panel re-render
|
||||||
|
handleNodeSelect(nodeId, true)
|
||||||
|
setTimeout(() => {
|
||||||
|
handleNodeSelect(nodeId)
|
||||||
|
}, 0)
|
||||||
|
}, [onHide, nodeId, inputString, handleNodeSelect, handleCurlImport])
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Modal
|
||||||
|
title={t('workflow.nodes.http.curl.title')}
|
||||||
|
isShow={isShow}
|
||||||
|
onClose={onHide}
|
||||||
|
className='!w-[400px] !max-w-[400px] !p-4'
|
||||||
|
>
|
||||||
|
<div>
|
||||||
|
<textarea
|
||||||
|
value={inputString}
|
||||||
|
className='w-full my-3 p-3 text-sm text-gray-900 border-0 rounded-lg grow bg-gray-100 focus:outline-none focus:ring-1 focus:ring-inset focus:ring-gray-200 h-40'
|
||||||
|
onChange={e => setInputString(e.target.value)}
|
||||||
|
placeholder={t('workflow.nodes.http.curl.placeholder')!}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div className='mt-4 flex justify-end space-x-2'>
|
||||||
|
<Button className='!w-[95px]' onClick={onHide} >{t('common.operation.cancel')}</Button>
|
||||||
|
<Button className='!w-[95px]' variant='primary' onClick={handleSave} > {t('common.operation.save')}</Button>
|
||||||
|
</div>
|
||||||
|
</Modal>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export default React.memo(CurlPanel)
|
|
@ -8,11 +8,13 @@ import EditBody from './components/edit-body'
|
||||||
import AuthorizationModal from './components/authorization'
|
import AuthorizationModal from './components/authorization'
|
||||||
import type { HttpNodeType } from './types'
|
import type { HttpNodeType } from './types'
|
||||||
import Timeout from './components/timeout'
|
import Timeout from './components/timeout'
|
||||||
|
import CurlPanel from './components/curl-panel'
|
||||||
import cn from '@/utils/classnames'
|
import cn from '@/utils/classnames'
|
||||||
import Field from '@/app/components/workflow/nodes/_base/components/field'
|
import Field from '@/app/components/workflow/nodes/_base/components/field'
|
||||||
import Split from '@/app/components/workflow/nodes/_base/components/split'
|
import Split from '@/app/components/workflow/nodes/_base/components/split'
|
||||||
import OutputVars, { VarItem } from '@/app/components/workflow/nodes/_base/components/output-vars'
|
import OutputVars, { VarItem } from '@/app/components/workflow/nodes/_base/components/output-vars'
|
||||||
import { Settings01 } from '@/app/components/base/icons/src/vender/line/general'
|
import { Settings01 } from '@/app/components/base/icons/src/vender/line/general'
|
||||||
|
import { FileArrow01 } from '@/app/components/base/icons/src/vender/line/files'
|
||||||
import type { NodePanelProps } from '@/app/components/workflow/types'
|
import type { NodePanelProps } from '@/app/components/workflow/types'
|
||||||
import BeforeRunForm from '@/app/components/workflow/nodes/_base/components/before-run-form'
|
import BeforeRunForm from '@/app/components/workflow/nodes/_base/components/before-run-form'
|
||||||
import ResultPanel from '@/app/components/workflow/run/result-panel'
|
import ResultPanel from '@/app/components/workflow/run/result-panel'
|
||||||
|
@ -53,6 +55,10 @@ const Panel: FC<NodePanelProps<HttpNodeType>> = ({
|
||||||
inputVarValues,
|
inputVarValues,
|
||||||
setInputVarValues,
|
setInputVarValues,
|
||||||
runResult,
|
runResult,
|
||||||
|
isShowCurlPanel,
|
||||||
|
showCurlPanel,
|
||||||
|
hideCurlPanel,
|
||||||
|
handleCurlImport,
|
||||||
} = useConfig(id, data)
|
} = useConfig(id, data)
|
||||||
// To prevent prompt editor in body not update data.
|
// To prevent prompt editor in body not update data.
|
||||||
if (!isDataReady)
|
if (!isDataReady)
|
||||||
|
@ -64,6 +70,7 @@ const Panel: FC<NodePanelProps<HttpNodeType>> = ({
|
||||||
<Field
|
<Field
|
||||||
title={t(`${i18nPrefix}.api`)}
|
title={t(`${i18nPrefix}.api`)}
|
||||||
operations={
|
operations={
|
||||||
|
<div className='flex'>
|
||||||
<div
|
<div
|
||||||
onClick={showAuthorization}
|
onClick={showAuthorization}
|
||||||
className={cn(!readOnly && 'cursor-pointer hover:bg-gray-50', 'flex items-center h-6 space-x-1 px-2 rounded-md ')}
|
className={cn(!readOnly && 'cursor-pointer hover:bg-gray-50', 'flex items-center h-6 space-x-1 px-2 rounded-md ')}
|
||||||
|
@ -74,6 +81,16 @@ const Panel: FC<NodePanelProps<HttpNodeType>> = ({
|
||||||
<span className='ml-1 text-gray-700'>{t(`${i18nPrefix}.authorization.${inputs.authorization.type}`)}</span>
|
<span className='ml-1 text-gray-700'>{t(`${i18nPrefix}.authorization.${inputs.authorization.type}`)}</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
<div
|
||||||
|
onClick={showCurlPanel}
|
||||||
|
className={cn(!readOnly && 'cursor-pointer hover:bg-gray-50', 'flex items-center h-6 space-x-1 px-2 rounded-md ')}
|
||||||
|
>
|
||||||
|
{!readOnly && <FileArrow01 className='w-3 h-3 text-gray-500' />}
|
||||||
|
<div className='text-xs font-medium text-gray-500'>
|
||||||
|
{t(`${i18nPrefix}.curl.title`)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
<ApiInput
|
<ApiInput
|
||||||
|
@ -180,7 +197,15 @@ const Panel: FC<NodePanelProps<HttpNodeType>> = ({
|
||||||
result={<ResultPanel {...runResult} showSteps={false} />}
|
result={<ResultPanel {...runResult} showSteps={false} />}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
</div >
|
{(isShowCurlPanel && !readOnly) && (
|
||||||
|
<CurlPanel
|
||||||
|
nodeId={id}
|
||||||
|
isShow
|
||||||
|
onHide={hideCurlPanel}
|
||||||
|
handleCurlImport={handleCurlImport}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -164,6 +164,23 @@ const useConfig = (id: string, payload: HttpNodeType) => {
|
||||||
setRunInputData(newPayload)
|
setRunInputData(newPayload)
|
||||||
}, [setRunInputData])
|
}, [setRunInputData])
|
||||||
|
|
||||||
|
// curl import panel
|
||||||
|
const [isShowCurlPanel, {
|
||||||
|
setTrue: showCurlPanel,
|
||||||
|
setFalse: hideCurlPanel,
|
||||||
|
}] = useBoolean(false)
|
||||||
|
|
||||||
|
const handleCurlImport = useCallback((newNode: HttpNodeType) => {
|
||||||
|
const newInputs = produce(inputs, (draft: HttpNodeType) => {
|
||||||
|
draft.method = newNode.method
|
||||||
|
draft.url = newNode.url
|
||||||
|
draft.headers = newNode.headers
|
||||||
|
draft.params = newNode.params
|
||||||
|
draft.body = newNode.body
|
||||||
|
})
|
||||||
|
setInputs(newInputs)
|
||||||
|
}, [inputs, setInputs])
|
||||||
|
|
||||||
return {
|
return {
|
||||||
readOnly,
|
readOnly,
|
||||||
isDataReady,
|
isDataReady,
|
||||||
|
@ -203,6 +220,11 @@ const useConfig = (id: string, payload: HttpNodeType) => {
|
||||||
inputVarValues,
|
inputVarValues,
|
||||||
setInputVarValues,
|
setInputVarValues,
|
||||||
runResult,
|
runResult,
|
||||||
|
// curl import
|
||||||
|
isShowCurlPanel,
|
||||||
|
showCurlPanel,
|
||||||
|
hideCurlPanel,
|
||||||
|
handleCurlImport,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -408,6 +408,10 @@ const translation = {
|
||||||
writeLabel: 'Write Timeout',
|
writeLabel: 'Write Timeout',
|
||||||
writePlaceholder: 'Enter write timeout in seconds',
|
writePlaceholder: 'Enter write timeout in seconds',
|
||||||
},
|
},
|
||||||
|
curl: {
|
||||||
|
title: 'Import from cURL',
|
||||||
|
placeholder: 'Paste cURL string here',
|
||||||
|
},
|
||||||
},
|
},
|
||||||
code: {
|
code: {
|
||||||
inputVars: 'Input Variables',
|
inputVars: 'Input Variables',
|
||||||
|
|
|
@ -408,6 +408,10 @@ const translation = {
|
||||||
writeLabel: '写入超时',
|
writeLabel: '写入超时',
|
||||||
writePlaceholder: '输入写入超时(以秒为单位)',
|
writePlaceholder: '输入写入超时(以秒为单位)',
|
||||||
},
|
},
|
||||||
|
curl: {
|
||||||
|
title: '导入cURL',
|
||||||
|
placeholder: '粘贴 cURL 字符串',
|
||||||
|
},
|
||||||
},
|
},
|
||||||
code: {
|
code: {
|
||||||
inputVars: '输入变量',
|
inputVars: '输入变量',
|
||||||
|
|
Loading…
Reference in New Issue
Block a user