feat(web): add http-request、jinja-render node

This commit is contained in:
zhaoying
2025-12-30 18:54:14 +08:00
parent 61e6cc9e42
commit ca8d5f5cc3
12 changed files with 1041 additions and 221 deletions

View File

@@ -0,0 +1,141 @@
import { forwardRef, useEffect, useImperativeHandle, useState } from 'react';
import { Form, Select, Input } from 'antd';
import { useTranslation } from 'react-i18next';
import type { AuthConfigModalRef, HttpRequestConfigForm } from './types'
import RbModal from '@/components/RbModal'
const FormItem = Form.Item;
interface AuthConfigModalProps {
refresh: (values: HttpRequestConfigForm['auth']) => void;
}
const AuthConfigModal = forwardRef<AuthConfigModalRef, AuthConfigModalProps>(({
refresh,
}, ref) => {
const { t } = useTranslation();
const [visible, setVisible] = useState(false);
const [form] = Form.useForm<HttpRequestConfigForm['auth']>();
const values = Form.useWatch<HttpRequestConfigForm['auth']>([], form);
// 封装取消方法,添加关闭弹窗逻辑
const handleClose = () => {
setVisible(false);
form.resetFields();
};
const handleOpen = (data?: HttpRequestConfigForm['auth']) => {
if (data) {
form.setFieldsValue({
auth: !data.auth_type || data.auth_type === 'none' ? 'none' : 'api_key',
auth_type: !data.auth_type || data.auth_type === 'none' ? undefined : data.auth_type,
header: data.header,
api_key: data.api_key
})
}
setVisible(true);
};
// 封装保存方法,添加提交逻辑
const handleSave = () => {
form
.validateFields()
.then(() => {
const { auth, auth_type, ...rest } = values ?? {}
refresh({
auth_type: auth === 'none' ? 'none' : auth_type,
...rest
})
handleClose()
})
.catch((err) => {
console.log('err', err)
});
}
// 暴露给父组件的方法
useImperativeHandle(ref, () => ({
handleOpen,
handleClose
}));
useEffect(() => {
if (values?.auth === 'api_key') {
form.setFieldValue('auth_type', 'basic')
} else {
form.setFieldsValue({
auth_type: undefined,
header: undefined,
api_key: undefined
})
}
}, [values?.auth])
return (
<RbModal
title={t('workflow.config.http-request.auth')}
open={visible}
onCancel={handleClose}
okText={t('common.save')}
onOk={handleSave}
>
<Form
form={form}
layout="vertical"
initialValues={{
auth: 'none'
}}
>
<FormItem
name="auth"
label={t('workflow.config.http-request.authType')}
>
<Select
options={[
{ value: 'none', label: t('workflow.config.http-request.none') },
{ value: 'api_key', label: t('workflow.config.http-request.apiKey') },
]}
/>
</FormItem>
{values?.auth !== 'none' && <>
<FormItem
name="auth_type"
label={t('workflow.config.http-request.authType')}
>
<Select
options={[
{ value: 'basic', label: t('workflow.config.http-request.basic') },
{ value: 'bearer', label: t('workflow.config.http-request.bearer') },
{ value: 'custom', label: t('workflow.config.http-request.custom') },
]}
/>
</FormItem>
{values?.auth_type === 'custom' &&
<FormItem
name="header"
label={t('workflow.config.http-request.header')}
rules={[
{ required: true, message: t('common.pleaseEnter') }
]}
>
<Input placeholder={t('common.pleaseEnter')} />
</FormItem>
}
<FormItem
name="api_key"
label={t('workflow.config.http-request.api_key')}
rules={[
{ required: true, message: t('common.pleaseEnter') }
]}
>
<Input placeholder={t('common.pleaseEnter')} />
</FormItem>
</>}
</Form>
</RbModal>
);
});
export default AuthConfigModal;

View File

@@ -0,0 +1,232 @@
import { useState, useEffect } from 'react';
import { useTranslation } from 'react-i18next'
import { Button, Select, Table } from 'antd';
import { PlusOutlined, DeleteOutlined } from '@ant-design/icons';
import Editor from '../../Editor';
import type { Suggestion } from '../../Editor/plugin/AutocompletePlugin';
import Empty from '@/components/Empty';
import VariableSelect from '../VariableSelect';
export interface TableRow {
key: string;
name: string;
value: string;
type?: string;
}
interface EditableTableProps {
title?: string;
value?: Record<string, string> | TableRow[];
onChange?: (value: TableRow[]) => void;
options?: Suggestion[];
typeOptions?: {value: string, label: string}[]
}
const EditableTable: React.FC<EditableTableProps> = ({
title,
value,
onChange,
options = [],
typeOptions = []
}) => {
const { t } = useTranslation()
const [rows, setRows] = useState<TableRow[]>([]);
useEffect(() => {
console.log('EditableTable value', value)
if (Array.isArray(value)) {
setRows([...value])
} else if (value && Object.keys(value).length > 0) {
// Only update if rows are empty or significantly different
const valueEntries = Object.entries(value)
if (rows.length === 0 || rows.length !== valueEntries.length) {
setRows(valueEntries.map(([key, val], index) => {
console.log('val', val)
return {
key: index.toString(),
name: key || '',
value: val || '',
type: typeOptions.length > 0 ? typeOptions[0].value : undefined
}
}))
}
} else {
setRows([])
}
}, [JSON.stringify(value), typeOptions.length])
const handleChange = (key: string, field: 'name' | 'value' | 'type', val: string) => {
const newRows = [...rows.map(row =>
row.key === key ? { ...row, [field]: val } : row
)];
setRows(newRows);
onChange?.(newRows);
};
const handleAdd = () => {
const newKey = Date.now().toString();
if (typeOptions.length) {
setRows([...rows, { key: newKey, name: '', value: '', type: typeOptions[0].value }]);
} else {
setRows([...rows, { key: newKey, name: '', value: '' }]);
}
};
const handleDelete = (key: string, index: number) => {
console.log('index', index)
if (rows.length === 1) {
setRows([]);
onChange?.([]);
} else {
const newRows = rows.filter(row => row.key !== key);
setRows(newRows);
onChange?.(newRows);
}
};
const columns = typeOptions?.length > 0 ? [
{
title: t('workflow.config.name'),
dataIndex: 'name',
width: '45%',
render: (text: string, record: TableRow) => (
<Editor
options={options}
value={text}
height={32}
variant="outlined"
onChange={(value) => handleChange(record.key, 'name', value)}
/>
),
},
{
title: t('workflow.config.type'),
dataIndex: 'type',
width: '20%',
render: (text: string, record: TableRow) => (
<Select
value={text}
options={typeOptions}
onChange={(value) => {
console.log('value record', value)
handleChange(record.key, 'type', value)
}}
/>
),
},
{
title: t('workflow.config.value'),
dataIndex: 'value',
width: '45%',
render: (text: string, record: TableRow) => {
if (record.type === 'file') {
return (
<VariableSelect
options={options}
value={text}
onChange={(value) => {
console.log('value record', value)
handleChange(record.key, 'value', value)
}}
/>
)
}
return (
<Editor
options={options}
value={text}
height={32}
variant="outlined"
onChange={(value) => {
console.log('value record', value)
handleChange(record.key, 'value', value)
}}
/>
)
},
},
{
title: '',
width: '10%',
render: (_: any, record: TableRow, index: number) => (
<Button
type="text"
icon={<DeleteOutlined />}
onClick={() => handleDelete(record.key, index)}
/>
),
},
] : [
{
title: '键',
dataIndex: 'name',
width: '45%',
render: (text: string, record: TableRow) => (
<Editor
options={options}
value={text}
height={32}
variant="outlined"
onChange={(value) => handleChange(record.key, 'name', value)}
/>
),
},
{
title: '值',
dataIndex: 'value',
width: '45%',
render: (text: string, record: TableRow) => (
<Editor
options={options}
value={text}
height={32}
variant="outlined"
onChange={(value) => handleChange(record.key, 'value', value)}
/>
),
},
{
title: '',
width: '10%',
render: (_: any, record: TableRow, index: number) => (
<Button
type="text"
icon={<DeleteOutlined />}
onClick={() => handleDelete(record.key, index)}
/>
),
},
];
return (
<div className="rb:mb-4">
{title && <div className="rb:flex rb:items-center rb:mb-2 rb:justify-between">
<div className="rb:font-medium">{title}</div>
<Button
type="text"
icon={<PlusOutlined />}
onClick={handleAdd}
size="small"
/>
</div>}
<Table
columns={columns}
dataSource={rows}
pagination={false}
size="small"
locale={{ emptyText: <Empty size={88} /> }}
scroll={{ x: 'max-content' }}
/>
{!title &&
<Button type="dashed" onClick={handleAdd} block className='rb:mt-1'>
+{t('common.add')}
</Button>
}
</div>
);
};
export default EditableTable;

View File

@@ -0,0 +1,272 @@
import { type FC, useEffect, useRef } from "react";
import { useTranslation } from 'react-i18next'
import { Form, Row, Col, Select, Button, Divider, InputNumber, Switch, Input, Slider } from 'antd'
import Editor from '../../Editor'
import type { Suggestion } from '../../Editor/plugin/AutocompletePlugin'
import AuthConfigModal from './AuthConfigModal'
import type { AuthConfigModalRef, HttpRequestConfigForm } from './types'
import VariableSelect from "../VariableSelect";
import MessageEditor from '../MessageEditor'
import EditableTable, { type TableRow } from './EditableTable'
const HttpRequest: FC<{ options: Suggestion[]; }> = ({
options,
}) => {
const { t } = useTranslation()
const form = Form.useFormInstance();
const values = Form.useWatch([], form) || {}
const authConfigModalRef = useRef<AuthConfigModalRef>(null)
const handleChangeAuth = () => {
authConfigModalRef.current?.handleOpen(values?.auth)
}
const handleRefresh = (auth: HttpRequestConfigForm['auth']) => {
console.log('handleRefresh', auth)
form.setFieldsValue({ auth: {...auth} })
}
const handleChangeBodyContentType = (contentType: string) => {
const currentValues = form.getFieldsValue()
form.setFieldsValue({
body: {
...currentValues?.body,
content_type: contentType,
data: undefined
}
})
}
const updateObjectList = (data: TableRow[], key: string) => {
let obj: Record<string, string> = {}
if (data.length) {
data.forEach(vo => {
obj[vo.name] = vo.value
})
}
form.setFieldValue(key, obj)
}
console.log('HttpRequest', values)
return (
<>
<div className="rb:flex rb:items-center rb:justify-between rb:mb-4">
<div>API</div>
<Button onClick={handleChangeAuth}>{t('workflow.config.http-request.auth')}</Button>
</div>
<Row gutter={16}>
<Col span={8}>
<Form.Item name="method">
<Select
options={[
{ label: 'GET', value: 'GET' },
{ label: 'POST', value: 'POST' },
{ label: 'HEAD', value: 'HEAD' },
{ label: 'PATCH', value: 'PATCH' },
{ label: 'PUT', value: 'PUT' },
{ label: 'DELETE', value: 'DELETE' },
]}
/>
</Form.Item>
</Col>
<Col span={16}>
<Form.Item name="url">
<Editor options={options} variant="outlined" />
</Form.Item>
</Col>
</Row>
<Form.Item name="auth" hidden>
</Form.Item>
<Form.Item name="headers">
<EditableTable
title="HEADERS"
options={options}
onChange={(headers) => updateObjectList(headers, 'headers')}
/>
</Form.Item>
<Form.Item name="params">
<EditableTable
title="PARAMS"
options={options}
onChange={(params) => updateObjectList(params, 'params')}
/>
</Form.Item>
<Form.Item label="BODY">
<Form.Item name={['body', 'content_type']}>
<Select
placeholder={t('common.pleaseSelect')}
onChange={handleChangeBodyContentType}
options={[
{ label: 'none', value: 'none' },
{ label: 'form-data', value: 'form-data' },
{ label: 'x-www-form-urlencoded', value: 'x-www-form-urlencoded' },
{ label: 'JSON', value: 'json' },
{ label: 'raw', value: 'raw' },
{ label: 'binary', value: 'binary' },
]}
/>
</Form.Item>
{values?.body?.content_type === 'form-data' &&
<Form.Item name={['body', 'data']} noStyle>
<EditableTable
options={options}
onChange={(data) => {
form.setFieldsValue({
body: {
...form.getFieldValue('body'),
data
}
})
}}
typeOptions={[
{ label: 'text', value: 'text' },
{ label: 'file', value: 'file' }
]}
/>
</Form.Item>
}
{values?.body?.content_type === 'x-www-form-urlencoded' &&
<Form.Item name={['body', 'data']} noStyle>
<EditableTable
options={options}
onChange={(data) => {
const currentBody = form.getFieldValue('body') || {}
form.setFieldsValue({
body: { ...currentBody, data }
})
}}
/>
</Form.Item>
}
{values?.body?.content_type === 'json' &&
<Form.Item name={['body', 'data']}>
<MessageEditor
options={options}
isArray={false}
title="JSON"
/>
</Form.Item>
}
{values?.body?.content_type === 'raw' &&
<Form.Item name={['body', 'data']}>
<MessageEditor
options={options}
isArray={false}
title="RAW TEXT"
/>
</Form.Item>
}
{values?.body?.content_type === 'binary' &&
<Form.Item name={['body', 'data']}>
<VariableSelect
options={options}
/>
</Form.Item>
}
</Form.Item>
<Divider />
<Form.Item layout="horizontal" name="verify_ssl" label={t('workflow.config.http-request.verify_ssl')}>
<Switch />
</Form.Item>
<Divider />
<div>{t('workflow.config.http-request.timeouts')}</div>
<Form.Item
name={['timeouts', 'connect_timeout']}
label={t('workflow.config.http-request.connect_timeout')}
>
<InputNumber placeholder={t('common.pleaseEnter')} className="rb:w-full!" />
</Form.Item>
<Form.Item
name={['timeouts', 'read_timeout']}
label={t('workflow.config.http-request.read_timeout')}
>
<InputNumber placeholder={t('common.pleaseEnter')} className="rb:w-full!" />
</Form.Item>
<Form.Item
name={['timeouts', 'write_timeout']}
label={t('workflow.config.http-request.write_timeout')}
>
<InputNumber placeholder={t('common.pleaseEnter')} className="rb:w-full!" />
</Form.Item>
<Divider />
<Form.Item name={['retry', 'enable']} valuePropName="checked" layout="horizontal" label={t('workflow.config.http-request.retry')}>
<Switch />
</Form.Item>
{(values?.retry?.enable || typeof values?.retry?.max_attempts === 'number' || typeof values?.retry?.retry_interval === 'number') &&
<>
<Form.Item
name={['retry', 'max_attempts']}
label={t('workflow.config.http-request.max_attempts')}
>
<InputNumber placeholder={t('common.pleaseEnter')} className="rb:w-full!" />
</Form.Item>
<Form.Item
name={['retry', 'retry_interval']}
label={t('workflow.config.http-request.retry_interval')}
>
<InputNumber placeholder={t('common.pleaseEnter')} className="rb:w-full!" />
</Form.Item>
</>
}
<Divider />
<Form.Item layout="horizontal" name={['error_handle', 'method']} label={t('workflow.config.http-request.error_handle')}>
<Select
placeholder={t('common.pleaseSelect')}
options={[
{ value: 'none', label: t('workflow.config.http-request.none') },
{ value: 'default', label: t('workflow.config.http-request.default') },
{ value: 'branch', label: t('workflow.config.http-request.branch') },
]}
/>
</Form.Item>
{values?.error_handle?.method === 'default' &&
<>
<Form.Item
name={['error_handle', 'body']}
label="body"
>
<Input placeholder={t('common.pleaseEnter')} />
</Form.Item>
<Form.Item
name={['error_handle', 'status_code']}
label="status_code"
>
<InputNumber placeholder={t('common.pleaseEnter')} className="rb:w-full!" />
</Form.Item>
<Form.Item
name={['error_handle', 'headers']}
label="headers"
rules={[
{
validator: (_, value) => {
if (!value) return Promise.resolve();
try {
JSON.parse(value);
return Promise.resolve();
} catch {
return Promise.reject(new Error('Please enter valid JSON format'));
}
}
}
]}
>
<Input.TextArea placeholder={t('common.pleaseEnter')} />
</Form.Item>
</>
}
<AuthConfigModal
ref={authConfigModalRef}
refresh={handleRefresh}
/>
</>
);
};
export default HttpRequest;

View File

@@ -0,0 +1,44 @@
export interface HttpRequestConfigForm {
method?: string;
url?: string;
auth?: {
auth?: string;
auth_type?: string;
header?: string;
api_key?: string;
};
headers?: {
[key: string]: string;
};
params?: {
[key: string]: string;
};
body?: {
content_type?: string;
data: string | Record<string, string>;
};
verify_ssl?: boolean;
timeouts?: {
connect_timeout: number;
read_timeout: number;
write_timeout: number;
};
retry?: {
max_attempts: number;
retry_interval: number;
};
error_handle?: {
method: string;
default: {
body: string;
status_code: number;
headers: {
[key: string]: string;
};
};
};
}
export interface AuthConfigModalRef {
handleOpen: (vo?: HttpRequestConfigForm['auth']) => void;
}