import React, { createContext, createElement, forwardRef, useContext, useEffect, useImperativeHandle, useLayoutEffect, useRef, useState, } from 'react'; import { Alert, Button, Col, Collapse, Drawer, Form, Input, InputNumber, Row, Select, Steps, Switch, Table, Utils } from 'knowdesign'; import { FormInstance } from 'knowdesign/es/basic/form/Form'; import SwitchTab from '@src/components/SwitchTab'; import message from '@src/components/Message'; import api from '@src/api'; import { useParams } from 'react-router-dom'; import { regClusterName } from '@src/constants/reg'; import { IconFont } from '@knowdesign/icons'; const { Step } = Steps; export interface ConnectCluster { id: number; name: string; groupName: string; state: number; version: string; jmxProperties: string; clusterUrl: string; memberLeaderUrl: string; } export interface ConnectorPlugin { type: 'source' | 'sink'; version: string; className: string; helpDocLink: string; } interface ConnectorPluginConfigDefinition { name: string; type: string; required: boolean; defaultValue: string | null; importance: string; documentation: string; group: string; orderInGroup: number; width: string; displayName: string; dependents: string[]; } interface ConnectorPluginConfigValue { errors: string[]; name: string; recommendedValues: any[]; value: any; visible: boolean; } export interface ConnectorPluginConfig { name: string; errorCount: number; groups: string[]; configs: { definition: ConnectorPluginConfigDefinition; value: ConnectorPluginConfigValue; }[]; } interface FormConnectorConfigs { pluginConfig: { [key: string]: ConnectorPluginConfigDefinition[] }; connectorConfig?: { [key: string]: any }; } interface SubFormProps { visible: boolean; setSubmitLoading: (loading: boolean) => void; } export interface OperateInfo { type: 'create' | 'edit'; errors: { [key: string]: string[]; }; detail?: { connectClusterId: number; connectorName: string; connectorClassName: string; connectorType: 'source' | 'sink'; }; } const existFormItems = { basic: ['name', 'connector.class', 'tasks.max', 'key.converter', 'value.converter', 'header.converter'], transforms: ['transforms'], errorHandling: [ 'errors.retry.timeout', 'errors.retry.delay.max.ms', 'errors.tolerance', 'errors.log.enable', 'errors.log.include.messages', ], }; const getExistFormItems = (type: 'source' | 'sink') => { return [...existFormItems.basic, ...existFormItems.transforms, ...existFormItems.errorHandling, type === 'sink' ? 'topics' : ''].filter( (k) => k ); }; const StepsFormContent = createContext< OperateInfo & { forms: { current: { [key: string]: FormInstance } }; } >({ type: 'create', errors: {}, forms: { current: {} }, }); function useStepForm(key: string | number) { const { forms } = useContext(StepsFormContent); const [form] = Form.useForm(); let formInstace = form; if (forms.current[key]) { formInstace = forms.current[key] as FormInstance; } else { forms.current[key] = formInstace; } return [formInstace]; } // 步骤一:设置插件类型 const StepFormFirst = (props: SubFormProps) => { const { clusterId } = useParams<{ clusterId: string; }>(); const [form] = useStepForm(0); const { type, detail } = useContext(StepsFormContent); const isEdit = type === 'edit'; const [connectClusters, setConnectClusters] = useState<{ label: string; value: number }[]>([]); const [selectedConnectClusterId, setSelectedConnectClusterId] = useState(detail?.connectClusterId); const [input, setInput] = useState(''); const [plugins, setPlugins] = useState([]); const [selectedPlugin, setSelectedPlugin] = useState(detail?.connectorClassName); const [pluginType, setPluginType] = useState<'source' | 'sink'>((detail?.connectorType.toLowerCase() as 'source' | 'sink') || 'source'); const [loading, setLoading] = useState(false); const getConnectClusters = () => { return Utils.request(api.getConnectClusters(clusterId)).then((res: ConnectCluster[]) => { const arr = res.map(({ name, id }) => ({ label: name || '-', value: id, })); setConnectClusters(arr); form.setFieldsValue({ connectClusters: arr, }); }); }; const getConnectorPlugins = () => { setLoading(true); return Utils.request(api.getConnectorPlugins(selectedConnectClusterId)) .then((res: ConnectorPlugin[]) => { setPlugins(res); }) .finally(() => setLoading(false)); }; const getConnectorPluginConfig = (pluginName: string) => { props.setSubmitLoading(true); Promise.all( [ Utils.request(api.getConnectorPluginConfig(selectedConnectClusterId, pluginName)), isEdit ? Utils.request(api.getCurPluginConfig(selectedConnectClusterId, detail.connectorName)) : undefined, ].filter((r) => r) ) .then((res: [ConnectorPluginConfig, { [key: string]: any }]) => { const [pluginConfig, connectorConfigs] = res; // 格式化插件配置 const result: FormConnectorConfigs = { pluginConfig: {}, }; pluginConfig.configs.forEach(({ definition }) => { if (!getExistFormItems(pluginType).includes(definition.name)) { const pluginConfigs = result.pluginConfig; const group = definition.group || 'Others'; pluginConfigs[group] ? pluginConfigs[group].push(definition) : (pluginConfigs[group] = [definition]); } }); Object.values(result.pluginConfig).forEach((arr) => arr.sort((a, b) => a.orderInGroup - b.orderInGroup)); // 加入当前 connector 的配置 if (isEdit) { result.connectorConfig = connectorConfigs; } Object.keys(result).length && form.setFieldsValue({ configs: result, }); }) .finally(() => props.setSubmitLoading(false)); }; useEffect(() => { if (selectedPlugin) { getConnectorPluginConfig(selectedPlugin); } }, [selectedPlugin]); useEffect(() => { if (selectedConnectClusterId) { getConnectorPlugins(); } }, [selectedConnectClusterId]); useEffect(() => { getConnectClusters(); }, []); return (
{ setInput(e.target.value); }} />
{ return ( {value} {record?.helpDocLink && ( window.open(record.helpDocLink)} > help )} ); }, }, ]} dataSource={plugins.filter((plugin) => plugin.type === pluginType && (!input || plugin.className.includes(input)))} pagination={false} rowSelection={{ type: 'radio', preserveSelectedRowKeys: false, selectedRowKeys: [selectedPlugin], getCheckboxProps: (record) => { return { disabled: isEdit && record.className !== selectedPlugin, }; }, onChange: (keys) => { setSelectedPlugin(keys[0] as string); form.setFieldsValue({ connectorClassName: keys[0], }); }, }} />
{ if (!value) { return Promise.reject('请选择 Connector 插件'); } return Promise.resolve(); }, }, ]} /> { if (!form.getFieldValue('connectorClassName')) { return Promise.resolve(true); } if (!value) { return Promise.reject(isEdit ? '插件或 connector 配置获取失败' : '插件配置获取失败,请重新选择插件'); } return Promise.resolve(); }, }, ]} />
) : ( <> ); }} ); }; // 步骤二:基础设置 const StepFormSecond = (props: SubFormProps) => { const { clusterId } = useParams<{ clusterId: string; }>(); const [prevForm] = useStepForm(0); const [form] = useStepForm(1); const [topicData, setTopicData] = useState([]); const { type, detail, errors } = useContext(StepsFormContent); const isEdit = type === 'edit'; const connectorConfig = (prevForm.getFieldValue('configs') as FormConnectorConfigs)?.connectorConfig; const getTopicList = () => { Utils.request(api.getTopicMetaList(Number(clusterId)), { method: 'GET', }).then((res: any) => { const dataDe = res || []; const dataHandle = dataDe.map((item: any) => { return { ...item, key: item.topicName, label: item.topicName, value: item.topicName, }; }); setTopicData(dataHandle); }); }; useEffect(() => { getTopicList(); }, []); useEffect(() => { connectorConfig && form.setFieldsValue({ topics: typeof connectorConfig['topics'] === 'string' ? connectorConfig['topics'].split(',').map((i: string) => i.trim()) : undefined, }); }, [topicData, connectorConfig]); useEffect(() => { const curConfig = connectorConfig || {}; form.setFieldsValue({ 'connector.class': curConfig['connector.class'] || prevForm.getFieldValue('connectorClassName'), 'tasks.max': curConfig['tasks.max'] || 1, 'key.converter': curConfig['key.converter'], 'value.converter': curConfig['value.converter'], 'header.converter': curConfig['header.converter'], }); }, [connectorConfig]); useEffect(() => { form.setFieldsValue({ 'connector.class': prevForm.getFieldValue('connectorClassName'), }); }, [prevForm.getFieldValue('connectorClassName')]); useEffect(() => { form.setFields([ ...existFormItems.basic.map((name) => ({ name, errors: errors[name] || [] })), { name: 'topics', errors: prevForm.getFieldValue('connectorType') === 'sink' ? errors['topics'] || [] : [] }, ]); }, [errors]); return (
64) { return Promise.reject('Connector 名称长度限制在1~128字符'); } if (!new RegExp(regClusterName).test(value)) { return Promise.reject( "Connector 名称支持中英文、数字、特殊字符 ! # $ % & ' ( ) * + , - . / : ; < = > ? @ [ ] ^ _ ` { | } ~" ); } return Utils.request(api.isConnectorExist(prevForm.getFieldValue('connectClusterId'), value)).then( (res: any) => { const data = res || {}; return data?.exist ? Promise.reject('Connector 名称重复') : Promise.resolve(); }, () => Promise.reject('连接超时! 请重试或检查服务') ); }, }, ]} >
Connector 插件类型: {form.getFieldValue('connector.class') || '-'}
value || null} > value || null} > value || null} > {/* Connector 类型为 Sink 时才有 */} {prevForm.getFieldValue('connectorType') === 'sink' && ( */} ) : type.toUpperCase() === 'INT' || type.toUpperCase() === 'LONG' ? ( ) : type.toUpperCase() === 'BOOLEAN' ? ( ) : type.toUpperCase() === 'PASSWORD' ? ( ) : ( )} ); })} ); })} )}
); }; const steps = [ { title: '设置插件类型', content: StepFormFirst, }, { title: '基础设置', content: StepFormSecond, }, { title: 'Transforms', content: StepFormThird, }, { title: 'Error Handling', content: StepFormForth, }, { title: '高级设置', content: StepFormFifth, }, ]; export default forwardRef( ( props: { refresh: () => void; }, ref ) => { const [visible, setVisible] = useState(false); const [jsonRef, setJsonRef] = useState({}); const [currentStep, setCurrentStep] = useState(0); const [stepInitState, setStepInitState] = useState([1]); const [submitLoading, setSubmitLoading] = useState(false); const [operateInfo, setOperateInfo] = useState({ type: undefined, errors: {}, }); const stepsFormRef = useRef<{ [key: string]: FormInstance; }>({}); const onOpen = (type: OperateInfo['type'], jsonRef: any, detail?: OperateInfo['detail']) => { if (type === 'create') { setStepInitState([1]); } else { setStepInitState([1, 2, 3, 4]); } setOperateInfo({ type, detail, errors: {}, }); setJsonRef(jsonRef); setVisible(true); }; const onClose = () => { Object.values(stepsFormRef.current).forEach((form) => { form.resetFields(); }); stepsFormRef.current = {}; setVisible(false); setCurrentStep(0); setStepInitState([]); }; const turnTo = (jumpStep: number) => { if (submitLoading) { message.warning('加载中,请稍后重试'); return; } if (jumpStep > currentStep) { const prevInit = stepInitState[jumpStep - 1]; if (!prevInit) { message.warning('请按照顺序填写'); } else { stepsFormRef.current[currentStep].validateFields().then(() => { const prevStep = jumpStep - 1; if (currentStep < prevStep) { stepsFormRef.current[prevStep] .validateFields() .then(() => { setStepInitState((prev) => { const cur = [...prev]; cur[jumpStep] = 1; return cur; }); setCurrentStep(jumpStep); }) .catch(() => { setCurrentStep(prevStep); }); } else { setStepInitState((prev) => { const cur = [...prev]; cur[jumpStep] = 1; return cur; }); setCurrentStep(jumpStep); } }); } } else { setCurrentStep(jumpStep); } }; // 校验所有表单 const validateForms = ( callback: (info: { success?: { connectClusterId: number; connectorName: string; configs: { [key: string]: any; }; }; error?: any; }) => void ) => { const promises: Promise[] = []; Object.values(stepsFormRef.current).forEach((form, i) => { const promise = form .validateFields() .then((res) => { return res; }) .catch(() => { return Promise.reject(i); }); promises.push(promise); }); Promise.all(promises).then( (res) => { const result = { ...res[1], ...res[3], ...res[4], }; // topics 配置格式化 res[1].topics && (result.topics = (res[1].topics as string[]).join(', ')); // transforms 配置格式化 res[2].transforms && (res[2].transforms as string) .split('\n') .filter((l) => l) .forEach((l) => { const [k, ...v] = l.split('='); result[k] = v.join('='); }); callback({ success: { connectClusterId: res[0].connectClusterId, connectorName: result['name'], configs: result, }, }); }, (error) => { callback({ error, }); } ); }; const toJsonMode = () => { validateForms((info) => { if (info.error) { message.warning('校验失败,请检查填写内容'); setCurrentStep(info.error); } else { let curClusterName = ''; stepsFormRef.current[0].getFieldValue('connectClusters').some((cluster: { label: string; value: number }) => { if (cluster.value === info.success.connectClusterId) { curClusterName = cluster.label; } }); (jsonRef as any)?.onOpen(operateInfo.type, curClusterName, info.success.configs); onClose(); } }); }; const onSubmit = () => { validateForms((info) => { if (info.error) { message.warning('校验失败,请检查填写内容'); setCurrentStep(info.error); } else { setSubmitLoading(true); Object.entries(info.success.configs).forEach(([key, val]) => { if (val === null) { delete info.success.configs[key]; } }); Utils.put(api.validateConnectorConfig, info.success).then( (res: ConnectorPluginConfig) => { if (res) { if (res?.errorCount > 0) { const errors: OperateInfo['errors'] = {}; res?.configs ?.filter((config) => config.value.errors.length !== 0) .forEach(({ value }) => { if (value.name.includes('transforms.')) { errors['transforms'] = (errors['transforms'] || []).concat(value.errors); } else { errors[value.name] = value.errors; } }); setOperateInfo((cur) => ({ ...cur, errors, })); // 步骤跳转 const items = getExistFormItems(stepsFormRef.current[0].getFieldValue('connectorType')); const keys = Object.keys(errors).filter((key) => items.includes(key)); let jumpStep = 4; keys.forEach((key) => { Object.values(existFormItems).some((items, i) => { if (items.includes(key)) { jumpStep > i + 1 && (jumpStep = i + 1); return true; } return false; }); }); setCurrentStep(jumpStep); setSubmitLoading(false); message.warning('字段校验失败,请检查'); } else { if (operateInfo.type === 'create') { Utils.post(api.connectorsOperates, info.success) .then(() => { message.success('新建成功'); onClose(); props?.refresh(); }) .finally(() => setSubmitLoading(false)); } else { Utils.put(api.updateConnectorConfig, info.success) .then(() => { message.success('编辑成功'); props?.refresh(); onClose(); }) .finally(() => setSubmitLoading(false)); } } } else { setSubmitLoading(false); message.error('接口校验出错,请重新提交'); } }, () => setSubmitLoading(false) ); } }); }; useImperativeHandle(ref, () => ({ onOpen, onClose, })); return ( {operateInfo.type && visible && ( <> turnTo(cur)}> {steps.map(({ title }) => ( ))}
{steps.map((step, i) => { return createElement(step.content, { visible: i === currentStep, setSubmitLoading, }); })} {currentStep === steps.length - 1 && ( 如果你想自定义更多配置,可以点击 继续补充 } /> )}
{currentStep > 0 && ( )} {currentStep < steps.length - 1 && ( )} {currentStep === steps.length - 1 && ( )}
)}
); } );