feat(datasource): Database connection Renewal (#2359)

Co-authored-by: aries_ckt <916701291@qq.com>
Co-authored-by: Fangyin Cheng <staneyffer@gmail.com>
Co-authored-by: ‘yanzhiyonggit config --global user.email git config --global user.name ‘yanzhiyong <zhiyong.yan@clife.cn>
This commit is contained in:
云锋
2025-02-22 18:44:21 +08:00
committed by GitHub
parent e4b329ee21
commit 94b51284e0
132 changed files with 689 additions and 501 deletions

View File

@@ -1,4 +1,4 @@
import { SupportModelParams } from '@/types/model';
import { ConfigurableParams } from '@/types/common';
import { Checkbox, Form, FormInstance, Input, InputNumber, Select } from 'antd';
import { useEffect } from 'react';
import NestedFormFields from './nested-form-fields';
@@ -7,7 +7,7 @@ interface ParamValues {
[key: string]: string | number | boolean | Record<string, any>;
}
function ModelParams({ params, form }: { params: Array<SupportModelParams> | null; form: FormInstance<any> }) {
function ConfigurableForm({ params, form }: { params: Array<ConfigurableParams> | null; form: FormInstance<any> }) {
useEffect(() => {
if (params) {
const initialValues: ParamValues = {};
@@ -53,6 +53,7 @@ function ModelParams({ params, form }: { params: Array<SupportModelParams> | nul
});
return normalized;
};
// Override the original submit method of the form
const originalSubmit = form.submit;
form.submit = () => {
@@ -62,12 +63,12 @@ function ModelParams({ params, form }: { params: Array<SupportModelParams> | nul
originalSubmit.call(form);
};
function renderItem(param: SupportModelParams) {
function renderItem(param: ConfigurableParams) {
if (param.nested_fields) {
return (
<NestedFormFields
parentName={param.param_name}
fields={param.nested_fields as Record<string, SupportModelParams[]>}
fields={param.nested_fields as Record<string, ConfigurableParams[]>}
form={form}
/>
);
@@ -75,8 +76,10 @@ function ModelParams({ params, form }: { params: Array<SupportModelParams> | nul
const type = param.param_type.toLowerCase();
const isFixed = param.ext_metadata?.tags?.includes('fixed');
const isPrivacy = param.ext_metadata?.tags?.includes('privacy');
if (type === 'str' || type === 'string') {
// Handle dropdown selection box
if (param.valid_values) {
return (
<Select disabled={isFixed}>
@@ -88,20 +91,30 @@ function ModelParams({ params, form }: { params: Array<SupportModelParams> | nul
</Select>
);
}
// Handle password input box
if (isPrivacy) {
return <Input.Password disabled={isFixed} autoComplete='new-password' placeholder='请输入密码' />;
}
// Handle normal text input box
return <Input disabled={isFixed} />;
}
if (type === 'int' || type === 'integer' || type === 'number' || type === 'float') {
return <InputNumber className='w-full' disabled={isFixed} />;
}
if (type === 'bool' || type === 'boolean') {
return <Checkbox disabled={isFixed} />;
}
return <Input disabled={isFixed} />;
}
return (
<div className='space-y-4'>
{params?.map((param: SupportModelParams) => (
{params?.map((param: ConfigurableParams) => (
<Form.Item
key={param.param_name}
label={<p className='whitespace-normal overflow-wrap-break-word'>{param.label || param.param_name}</p>}
@@ -122,4 +135,4 @@ function ModelParams({ params, form }: { params: Array<SupportModelParams> | nul
);
}
export default ModelParams;
export default ConfigurableForm;

View File

@@ -1,10 +1,10 @@
import { SupportModelParams } from '@/types/model';
import { ConfigurableParams } from '@/types/common';
import { Checkbox, Form, FormInstance, Input, InputNumber, Select } from 'antd';
import React, { useEffect, useState } from 'react';
interface NestedFormFieldsProps {
parentName: string;
fields: Record<string, SupportModelParams[]>;
fields: Record<string, ConfigurableParams[]>;
form: FormInstance;
}
const NestedFormFields: React.FC<NestedFormFieldsProps> = ({ parentName, fields, form }) => {
@@ -39,7 +39,7 @@ const NestedFormFields: React.FC<NestedFormFieldsProps> = ({ parentName, fields,
});
};
const renderFormItem = (param: SupportModelParams) => {
const renderFormItem = (param: ConfigurableParams) => {
const type = param.param_type.toLowerCase();
// Use the complete field path
const fieldPath = [parentName, param.param_name];

View File

@@ -0,0 +1,149 @@
import { apiInterceptors, postDbAdd, postDbEdit, postDbTestConnect } from '@/client/api';
import { ConfigurableParams } from '@/types/common';
import { DBOption, DBType } from '@/types/db';
import { Button, Form, Input, Select, message } from 'antd';
import { useEffect, useState } from 'react';
import { useTranslation } from 'react-i18next';
import ConfigurableForm from '../common/configurable-form';
const { Option } = Select;
const FormItem = Form.Item;
interface DatabaseFormProps {
onCancel: () => void;
onSuccess: () => void;
dbTypeList: DBOption[];
editValue?: string;
choiceDBType?: DBType;
getFromRenderData?: ConfigurableParams[];
dbNames?: string[];
description?: string; // Add description prop
}
function DatabaseForm({
onCancel,
onSuccess,
dbTypeList,
editValue,
choiceDBType,
getFromRenderData,
dbNames = [],
description = '', // Default value for description
}: DatabaseFormProps) {
const { t } = useTranslation();
const [form] = Form.useForm();
const [loading, setLoading] = useState(false);
const [selectedType, setSelectedType] = useState<DBType | undefined>(choiceDBType);
const [params, setParams] = useState<Array<ConfigurableParams> | null>(getFromRenderData || null);
console.log('dbTypeList', dbTypeList);
console.log('editValue', editValue);
console.log('choiceDBType', choiceDBType);
useEffect(() => {
if (choiceDBType) {
setSelectedType(choiceDBType);
}
}, [choiceDBType]);
useEffect(() => {
if (editValue && getFromRenderData) {
setParams(getFromRenderData);
// set description
form.setFieldValue('description', description);
}
}, [editValue, getFromRenderData, description, form]);
const handleTypeChange = (value: DBType) => {
setSelectedType(value);
form.resetFields(['params']);
const selectedDBType = dbTypeList.find(type => type.value === value);
if (selectedDBType?.parameters) {
setParams(selectedDBType.parameters);
}
};
const handleSubmit = async (formValues: any) => {
try {
setLoading(true);
console.log('dbNames:', dbNames);
// Check if database name is duplicated
// if (!editValue && dbNames.includes(values.database)) {
// message.error(t('database_name_exists'));
// return;
// }
const { description, type, ...values } = formValues;
const data = {
type: selectedType,
params: values,
description: description || '',
};
// If in edit mode, add id
if (editValue) {
data.id = editValue;
}
console.log('Form submitted:', data);
const [testErr] = await apiInterceptors(postDbTestConnect(data));
if (testErr) return;
const [err] = await apiInterceptors((editValue ? postDbEdit : postDbAdd)(data));
if (err) {
message.error(err.message);
return;
}
message.success(t(editValue ? 'update_success' : 'create_success'));
onSuccess?.();
} catch (error) {
console.error('Failed to submit form:', error);
message.error(t(editValue ? 'update_failed' : 'create_failed'));
} finally {
setLoading(false);
}
};
return (
<Form
form={form}
layout='vertical'
onFinish={handleSubmit}
initialValues={{
type: selectedType,
}}
>
<FormItem
label={t('database_type')}
name='type'
rules={[{ required: true, message: t('please_select_database_type') }]}
>
<Select placeholder={t('select_database_type')} onChange={handleTypeChange} disabled={!!editValue}>
{dbTypeList.map(type => (
<Option key={type.value} value={type.value} disabled={type.disabled}>
{type.label}
</Option>
))}
</Select>
</FormItem>
{params && <ConfigurableForm params={params} form={form} />}
<FormItem label={t('description')} name='description'>
<Input.TextArea rows={2} placeholder={t('input_description')} />
</FormItem>
<div className='flex justify-end space-x-4 mt-6'>
<Button onClick={onCancel}>{t('cancel')}</Button>
<Button type='primary' htmlType='submit' loading={loading}>
{t('submit')}
</Button>
</div>
</Form>
);
}
export default DatabaseForm;

View File

@@ -1,217 +1,54 @@
/* eslint-disable react-hooks/exhaustive-deps */
import { addOmcDB, apiInterceptors, getSupportDBList, postDbAdd, postDbEdit, postDbTestConnect } from '@/client/api';
import { isFileDb } from '@/pages/construct/database';
import { DBOption, DBType, DbListResponse, PostDbParams } from '@/types/db';
import { useDebounceFn } from 'ahooks';
import { Button, Form, Input, InputNumber, Modal, Select, Spin, Tooltip, message } from 'antd';
import { useEffect, useMemo, useState } from 'react';
import { ConfigurableParams } from '@/types/common';
import { DBOption, DBType } from '@/types/db';
import { Modal } from 'antd';
import { useTranslation } from 'react-i18next';
import DatabaseForm from './database-form';
type DBItem = DbListResponse[0] & { db_arn?: string };
interface Props {
dbTypeList: DBOption[];
interface NewFormDialogProps {
open: boolean;
onClose: () => void;
onSuccess: () => void;
dbTypeList: DBOption[];
editValue?: string;
choiceDBType?: DBType;
editValue?: DBItem;
dbNames: string[];
onSuccess?: () => void;
onClose?: () => void;
getFromRenderData?: ConfigurableParams[];
dbNames?: string[];
dbTypeData?: DBOption[];
description?: string;
}
function FormDialog({ open, choiceDBType, dbTypeList, editValue, dbNames, onClose, onSuccess }: Props) {
const [loading, setLoading] = useState(false);
function FormDialog({
open,
onClose,
onSuccess,
dbTypeList,
editValue,
choiceDBType,
getFromRenderData,
dbNames,
description,
}: NewFormDialogProps) {
const { t } = useTranslation();
const [form] = Form.useForm<DBItem>();
const dbType = Form.useWatch('db_type', form);
const [omcDBList, setOmcDBList] = useState([]);
const [omcListLoading, setOmcListLoading] = useState(false);
const fileDb = useMemo(() => isFileDb(dbTypeList, dbType), [dbTypeList, dbType]);
useEffect(() => {
if (choiceDBType) {
form.setFieldValue('db_type', choiceDBType);
}
}, [choiceDBType]);
useEffect(() => {
if (editValue) {
form.setFieldsValue({ ...editValue });
if (editValue.db_type === 'omc') {
form.setFieldValue('db_arn', editValue.db_path);
}
}
}, [editValue]);
useEffect(() => {
if (!open) {
form.resetFields();
}
}, [open]);
const onFinish = async (val: DBItem) => {
const { db_host, db_path, db_port, db_type, ...params } = val;
setLoading(true);
if (db_type === 'omc') {
const item = omcDBList?.find((item: any) => item.arn === val.db_name) as any;
try {
const [err] = await apiInterceptors(
addOmcDB({
db_type: 'omc',
file_path: val.db_arn || '',
comment: val.comment,
db_name: item?.dbName || val.db_name,
}),
);
if (err) {
message.error(err.message);
return;
}
message.success('success');
onSuccess?.();
} catch (e: any) {
message.error(e.message);
} finally {
setLoading(false);
}
}
if (!editValue && dbNames.some(item => item === params.db_name)) {
message.error('The database already exists!');
return;
}
const data: PostDbParams = {
db_host: fileDb ? undefined : db_host,
db_port: fileDb ? undefined : db_port,
db_type: db_type,
file_path: fileDb ? db_path : undefined,
...params,
};
try {
const [testErr] = await apiInterceptors(postDbTestConnect(data));
if (testErr) return;
const [err] = await apiInterceptors((editValue ? postDbEdit : postDbAdd)(data));
if (err) {
message.error(err.message);
return;
}
message.success('success');
onSuccess?.();
} catch (e: any) {
message.error(e.message);
} finally {
setLoading(false);
}
};
const { run: fetchOmcList } = useDebounceFn(
async (name: string) => {
setOmcListLoading(true);
const [_, data = []] = (await apiInterceptors(getSupportDBList(name))) as any;
setOmcListLoading(false);
setOmcDBList(data.map((item: any) => ({ ...item, label: item.dbName, value: item.arn })));
},
{
wait: 500,
},
);
const lockDBType = useMemo(() => !!editValue || !!choiceDBType, [editValue, choiceDBType]);
return (
<Modal
title={t(editValue ? 'edit_database' : 'add_database')}
open={open}
width={800}
title={editValue ? t('Edit') : t('create_database')}
maskClosable={false}
footer={null}
onCancel={onClose}
footer={null}
width={800}
destroyOnClose
>
<Form form={form} className='pt-2' labelCol={{ span: 6 }} labelAlign='left' onFinish={onFinish}>
<Form.Item name='db_type' label='DB Type' className='mb-6' rules={[{ required: true }]}>
<Select aria-readonly={lockDBType} disabled={lockDBType} options={dbTypeList} />
</Form.Item>
{form.getFieldValue('db_type') === 'omc' ? (
<Form.Item name='db_name' label='DB Name' className='mb-6' rules={[{ required: true }]}>
<Select
optionRender={(option, { index }) => {
const item = omcDBList[index] as any;
return (
<div key={option.value} className='flex flex-col'>
<span className='text-[18px]'>{item?.dbName}</span>
<span>
<span>env: </span>
<span className='text-gray-500'>{item.env}</span>
</span>
<span>
<span>account: </span>
<span className='text-gray-500'>{item.account}</span>
</span>
<span>
<span>searchName: </span>
<Tooltip title={item.searchName}>
<span className='text-gray-500'>{item.searchName}</span>
</Tooltip>
</span>
</div>
);
}}
notFoundContent={omcListLoading ? <Spin size='small' /> : null}
showSearch
options={omcDBList}
onSearch={fetchOmcList}
onSelect={searchName => {
const item = omcDBList?.find((item: any) => item.value === searchName) as any;
form.setFieldsValue({
db_arn: item?.arn,
});
}}
/>
</Form.Item>
) : (
<Form.Item name='db_name' label='DB Name' className='mb-3' rules={[{ required: true }]}>
<Input readOnly={!!editValue} disabled={!!editValue} />
</Form.Item>
)}
{fileDb === true && (
<Form.Item name='db_path' label='Path' className='mb-6' rules={[{ required: true }]}>
<Input />
</Form.Item>
)}
{fileDb === false && form.getFieldValue('db_type') !== 'omc' && (
<>
<Form.Item name='db_user' label='Username' className='mb-6' rules={[{ required: true }]}>
<Input />
</Form.Item>
<Form.Item name='db_pwd' label='Password' className='mb-6' rules={[{ required: false }]}>
<Input type='password' />
</Form.Item>
<Form.Item name='db_host' label='Host' className='mb-6' rules={[{ required: true }]}>
<Input />
</Form.Item>
<Form.Item name='db_port' label='Port' className='mb-6' rules={[{ required: true }]}>
<InputNumber min={1} step={1} max={65535} />
</Form.Item>
</>
)}
{form.getFieldValue('db_type') === 'omc' && (
<Form.Item name='db_arn' label='Arn' className='mb-6' rules={[{ required: true }]}>
<Input />
</Form.Item>
)}
<Form.Item name='comment' label='Remark' className='mb-6'>
<Input />
</Form.Item>
<Form.Item className='flex flex-row-reverse pt-1 mb-0'>
<Button htmlType='submit' type='primary' size='middle' className='mr-1' loading={loading}>
Save
</Button>
<Button size='middle' onClick={onClose}>
Cancel
</Button>
</Form.Item>
</Form>
<DatabaseForm
onCancel={onClose}
onSuccess={onSuccess}
dbTypeList={dbTypeList}
editValue={editValue}
choiceDBType={choiceDBType}
getFromRenderData={getFromRenderData}
dbNames={dbNames}
description={description}
/>
</Modal>
);
}

View File

@@ -1,11 +1,12 @@
import { apiInterceptors, createModel, getSupportModels } from '@/client/api';
import { renderModelIcon } from '@/components/chat/header/model-selector';
import { StartModelParams, SupportModel, SupportModelParams } from '@/types/model';
import { ConfigurableParams } from '@/types/common';
import { StartModelParams, SupportModel } from '@/types/model';
import { AutoComplete, Button, Form, Select, Tooltip, message } from 'antd';
import { useEffect, useState } from 'react';
import { useTranslation } from 'react-i18next';
import ReactMarkdown from 'react-markdown';
import ModelParams from './model-params';
import ConfigurableForm from '../common/configurable-form';
const { Option } = Select;
const FormItem = Form.Item;
@@ -18,7 +19,7 @@ function ModelForm({ onCancel, onSuccess }: { onCancel: () => void; onSuccess: (
const [_, setModels] = useState<Array<SupportModel> | null>([]);
const [selectedWorkerType, setSelectedWorkerType] = useState<string>();
const [selectedProvider, setSelectedProvider] = useState<string>();
const [params, setParams] = useState<Array<SupportModelParams> | null>(null);
const [params, setParams] = useState<Array<ConfigurableParams> | null>(null);
const [loading, setLoading] = useState<boolean>(false);
const [form] = Form.useForm();
@@ -215,7 +216,7 @@ function ModelForm({ onCancel, onSuccess }: { onCancel: () => void; onSuccess: (
/>
</FormItem>
<ModelParams params={params.filter(p => p.param_name !== 'name')} form={form} />
<ConfigurableForm params={params.filter(p => p.param_name !== 'name')} form={form} />
</>
)}