add import from cURL command of http request node

This commit is contained in:
hejl 2024-09-23 09:26:06 +08:00
parent e34f04380d
commit 101635d735
3 changed files with 180 additions and 9 deletions

View File

@ -0,0 +1,125 @@
'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 { useNodesInteractions } from '@/app/components/workflow/hooks'
type Props = {
nodeId: string
isShow: boolean
onHide: () => void
handleCurlImport: (node: HttpNodeType) => void
}
const parseCurl = (curlCommand: string): HttpNodeType => {
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':
node.method = (args[++i].replace(/^['"]|['"]$/g, '') as Method) || Method.get
break
case '-H':
case '--header':
node.headers += (node.headers ? '\n' : '') + args[++i].replace(/^['"]|['"]$/g, '')
break
case '-d':
case '--data':
case '--data-raw':
case '--data-binary':
node.body = { type: BodyType.rawText, data: args[++i].replace(/^['"]|['"]$/g, '') }
break
case '-F':
case '--form': {
if (node.body?.type !== BodyType.formData)
node.body = { type: BodyType.formData, data: '' }
const formData = args[++i].replace(/^['"]|['"]$/g, '')
const [key, ...valueParts] = formData.split('=')
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':
node.body = { type: BodyType.json, data: args[++i].replace(/^['"]|['"]$/g, '') }
break
default:
if (arg.startsWith('http') && !node.url)
node.url = arg
break
}
}
// 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 as HttpNodeType
}
const CurlPanel: FC<Props> = ({ nodeId, isShow, onHide, handleCurlImport }) => {
const [inputString, setInputString] = useState('')
const { handleNodeSelect } = useNodesInteractions()
const { t } = useTranslation()
const handleSave = useCallback(() => {
onHide()
const node = parseCurl(inputString)
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="Import From curl"
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="Paste curl string here"
/>
</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)

View File

@ -8,6 +8,7 @@ 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'
@ -52,6 +53,10 @@ const Panel: FC<NodePanelProps<HttpNodeType>> = ({
inputVarValues, inputVarValues,
setInputVarValues, setInputVarValues,
runResult, runResult,
isShowCurlPanel,
showCurlPanel,
hideCurlPanel,
handleCurlImport,
} = useConfig(id, data) } = useConfig(id, data)
return ( return (
@ -60,14 +65,25 @@ const Panel: FC<NodePanelProps<HttpNodeType>> = ({
<Field <Field
title={t(`${i18nPrefix}.api`)} title={t(`${i18nPrefix}.api`)}
operations={ operations={
<div <div className='flex'>
onClick={showAuthorization} <div
className={cn(!readOnly && 'cursor-pointer hover:bg-gray-50', 'flex items-center h-6 space-x-1 px-2 rounded-md ')} 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'> {!readOnly && <Settings01 className='w-3 h-3 text-gray-500' />}
{t(`${i18nPrefix}.authorization.authorization`)} <div className='text-xs font-medium text-gray-500'>
<span className='ml-1 text-gray-700'>{t(`${i18nPrefix}.authorization.${inputs.authorization.type}`)}</span> {t(`${i18nPrefix}.authorization.authorization`)}
<span className='ml-1 text-gray-700'>{t(`${i18nPrefix}.authorization.${inputs.authorization.type}`)}</span>
</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 && <Settings01 className='w-3 h-3 text-gray-500' />}
<div className='text-xs font-medium text-gray-500'>
{ '导入curl' }
</div>
</div> </div>
</div> </div>
} }
@ -176,7 +192,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>
) )
} }

View File

@ -143,6 +143,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,
inputs, inputs,
@ -181,6 +198,11 @@ const useConfig = (id: string, payload: HttpNodeType) => {
inputVarValues, inputVarValues,
setInputVarValues, setInputVarValues,
runResult, runResult,
// curl import
isShowCurlPanel,
showCurlPanel,
hideCurlPanel,
handleCurlImport,
} }
} }