mirror of
https://github.com/didi/KnowStreaming.git
synced 2026-01-08 15:52:15 +08:00
初始化3.0.0版本
This commit is contained in:
@@ -0,0 +1,630 @@
|
||||
/* eslint-disable react/display-name */
|
||||
|
||||
import { Button, Divider, Drawer, Form, Input, InputNumber, message, Radio, Select, Spin, Space, Utils } from 'knowdesign';
|
||||
import * as React from 'react';
|
||||
import { useIntl } from 'react-intl';
|
||||
import api from '../../api';
|
||||
import { regClusterName, regUsername } from '../../common/reg';
|
||||
import { bootstrapServersErrCodes, jmxErrCodes, zkErrCodes } from './config';
|
||||
import CodeMirrorFormItem from '@src/components/CodeMirrorFormItem';
|
||||
|
||||
const rows = 4;
|
||||
const lowKafkaVersion = '2.8.0';
|
||||
const clientPropertiesPlaceholder = `用于创建Kafka客户端进行信息获取的相关配置,
|
||||
例如开启SCRAM-SHA-256安全管控模式的集群需输入如下配置,
|
||||
未开启安全管控可不进行任何输入:
|
||||
{
|
||||
"security.protocol": "SASL_PLAINTEXT",
|
||||
"sasl.mechanism": "SCRAM-SHA-256",
|
||||
"sasl.jaas.config":
|
||||
"org.apache.kafka.common.security.scram.
|
||||
ScramLoginModule required username="xxxxxx"
|
||||
password="xxxxxx";"
|
||||
}
|
||||
`;
|
||||
|
||||
const AccessClusters = (props: any): JSX.Element => {
|
||||
const intl = useIntl();
|
||||
const [form] = Form.useForm();
|
||||
|
||||
const { afterSubmitSuccess, infoLoading, clusterInfo, visible } = props;
|
||||
const [loading, setLoading] = React.useState(false);
|
||||
const [security, setSecurity] = React.useState(clusterInfo?.security || 'None');
|
||||
const [extra, setExtra] = React.useState({
|
||||
versionExtra: '',
|
||||
zooKeeperExtra: '',
|
||||
bootstrapExtra: '',
|
||||
jmxExtra: '',
|
||||
});
|
||||
const [isLowVersion, setIsLowVersion] = React.useState<any>(false);
|
||||
const [zookeeperErrorStatus, setZookeeperErrorStatus] = React.useState<any>(false);
|
||||
|
||||
const lastFormItemValue = React.useRef({
|
||||
bootstrap: clusterInfo?.bootstrapServers || '',
|
||||
zookeeper: clusterInfo?.zookeeper || '',
|
||||
clientProperties: clusterInfo?.clientProperties || {},
|
||||
});
|
||||
|
||||
React.useEffect(() => {
|
||||
const showLowVersion = !(clusterInfo?.zookeeper || !clusterInfo?.kafkaVersion || clusterInfo?.kafkaVersion >= lowKafkaVersion);
|
||||
lastFormItemValue.current.bootstrap = clusterInfo?.bootstrapServers || '';
|
||||
lastFormItemValue.current.zookeeper = clusterInfo?.zookeeper || '';
|
||||
lastFormItemValue.current.clientProperties = clusterInfo?.clientProperties || {};
|
||||
setIsLowVersion(showLowVersion);
|
||||
setExtra({
|
||||
...extra,
|
||||
versionExtra: showLowVersion ? intl.formatMessage({ id: 'access.cluster.low.version.tip' }) : '',
|
||||
});
|
||||
form.setFieldsValue({ ...clusterInfo });
|
||||
}, [clusterInfo]);
|
||||
|
||||
const onHandleValuesChange = (value: any, allValues: any) => {
|
||||
Object.keys(value).forEach((key) => {
|
||||
switch (key) {
|
||||
case 'security':
|
||||
setSecurity(value.security);
|
||||
break;
|
||||
case 'zookeeper':
|
||||
setExtra({
|
||||
...extra,
|
||||
zooKeeperExtra: '',
|
||||
bootstrapExtra: '',
|
||||
jmxExtra: '',
|
||||
});
|
||||
break;
|
||||
case 'bootstrapServers':
|
||||
setExtra({
|
||||
...extra,
|
||||
zooKeeperExtra: '',
|
||||
bootstrapExtra: '',
|
||||
jmxExtra: '',
|
||||
});
|
||||
break;
|
||||
case 'kafkaVersion':
|
||||
setExtra({
|
||||
...extra,
|
||||
versionExtra: '',
|
||||
});
|
||||
break;
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
const onCancel = () => {
|
||||
form.resetFields();
|
||||
setLoading(false);
|
||||
setZookeeperErrorStatus(false);
|
||||
setIsLowVersion(false);
|
||||
setSecurity('None');
|
||||
setExtra({
|
||||
versionExtra: '',
|
||||
zooKeeperExtra: '',
|
||||
bootstrapExtra: '',
|
||||
jmxExtra: '',
|
||||
});
|
||||
lastFormItemValue.current = { bootstrap: '', zookeeper: '', clientProperties: {} };
|
||||
props.setVisible && props.setVisible(false);
|
||||
};
|
||||
|
||||
const onSubmit = () => {
|
||||
form.validateFields().then((res) => {
|
||||
let clientProperties = null;
|
||||
try {
|
||||
clientProperties = res.clientProperties && JSON.parse(res.clientProperties);
|
||||
} catch (err) {
|
||||
console.error(err);
|
||||
}
|
||||
|
||||
const params = {
|
||||
bootstrapServers: res.bootstrapServers,
|
||||
clientProperties: clientProperties || {},
|
||||
description: res.description || '',
|
||||
jmxProperties: {
|
||||
jmxPort: res.jmxPort,
|
||||
maxConn: res.maxConn,
|
||||
openSSL: res.security === 'Password',
|
||||
token: res.token,
|
||||
username: res.username,
|
||||
},
|
||||
kafkaVersion: res.kafkaVersion,
|
||||
name: res.name,
|
||||
zookeeper: res.zookeeper || '',
|
||||
};
|
||||
setLoading(true);
|
||||
if (!isNaN(clusterInfo?.id)) {
|
||||
Utils.put(api.phyCluster, {
|
||||
...params,
|
||||
id: clusterInfo?.id,
|
||||
})
|
||||
.then(() => {
|
||||
message.success('编辑成功');
|
||||
afterSubmitSuccess && afterSubmitSuccess();
|
||||
onCancel();
|
||||
})
|
||||
.finally(() => {
|
||||
setLoading(false);
|
||||
});
|
||||
} else {
|
||||
Utils.post(api.phyCluster, params)
|
||||
.then(() => {
|
||||
message.success('集群接入成功。注意:新接入集群数据稳定需要1-2分钟');
|
||||
afterSubmitSuccess && afterSubmitSuccess();
|
||||
onCancel();
|
||||
})
|
||||
.finally(() => {
|
||||
setLoading(false);
|
||||
});
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
const connectTest = () => {
|
||||
const bootstrapServers = form.getFieldValue('bootstrapServers');
|
||||
const zookeeper = form.getFieldValue('zookeeper');
|
||||
let clientProperties = {};
|
||||
try {
|
||||
clientProperties = form.getFieldValue('clientProperties') && JSON.parse(form.getFieldValue('clientProperties'));
|
||||
} catch (err) {
|
||||
console.error(`JSON.parse(form.getFieldValue('clientProperties')) ERROR: ${err}`);
|
||||
}
|
||||
|
||||
setLoading(true);
|
||||
setIsLowVersion(false);
|
||||
setZookeeperErrorStatus(false);
|
||||
|
||||
return Utils.post(api.kafkaValidator, {
|
||||
bootstrapServers: bootstrapServers || '',
|
||||
zookeeper: zookeeper || '',
|
||||
clientProperties,
|
||||
})
|
||||
.then((res: any) => {
|
||||
form.setFieldsValue({
|
||||
jmxPort: res.jmxPort,
|
||||
});
|
||||
|
||||
if (props.kafkaVersion.indexOf(res.kafkaVersion) > -1) {
|
||||
form.setFieldsValue({
|
||||
kafkaVersion: res.kafkaVersion,
|
||||
});
|
||||
} else {
|
||||
form.setFieldsValue({
|
||||
kafkaVersion: undefined,
|
||||
});
|
||||
}
|
||||
|
||||
form.setFieldsValue({
|
||||
zookeeper: zookeeper || res.zookeeper,
|
||||
});
|
||||
|
||||
const errList = res.errList || [];
|
||||
|
||||
const extraMsg = extra;
|
||||
|
||||
// 初始化信息为连接成功
|
||||
extraMsg.bootstrapExtra = bootstrapServers ? '连接成功' : '';
|
||||
extraMsg.zooKeeperExtra = zookeeper ? '连接成功' : '';
|
||||
|
||||
// 处理错误信息
|
||||
errList.forEach((item: any) => {
|
||||
const { code, message } = item;
|
||||
let modifyKey: 'bootstrapExtra' | 'zooKeeperExtra' | 'jmxExtra' | undefined;
|
||||
if (bootstrapServersErrCodes.includes(code)) {
|
||||
modifyKey = 'bootstrapExtra';
|
||||
} else if (zkErrCodes.includes(code)) {
|
||||
modifyKey = 'zooKeeperExtra';
|
||||
} else if (jmxErrCodes.includes(code)) {
|
||||
modifyKey = 'jmxExtra';
|
||||
}
|
||||
|
||||
if (modifyKey) {
|
||||
extraMsg[modifyKey] = `连接失败。${message}`;
|
||||
}
|
||||
});
|
||||
|
||||
// 如果kafkaVersion小于最低版本则提示
|
||||
const showLowVersion = !(clusterInfo?.zookeeper || !clusterInfo?.kafkaVersion || clusterInfo?.kafkaVersion >= lowKafkaVersion);
|
||||
setIsLowVersion(showLowVersion);
|
||||
setExtra({
|
||||
...extraMsg,
|
||||
versionExtra: showLowVersion ? intl.formatMessage({ id: 'access.cluster.low.version.tip' }) : '',
|
||||
});
|
||||
return res;
|
||||
})
|
||||
.finally(() => {
|
||||
setLoading(false);
|
||||
});
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<Drawer
|
||||
className="drawer-content drawer-access-cluster"
|
||||
onClose={onCancel}
|
||||
maskClosable={false}
|
||||
extra={
|
||||
<div className="operate-wrap">
|
||||
<Space>
|
||||
<Button size="small" onClick={onCancel}>
|
||||
取消
|
||||
</Button>
|
||||
<Button size="small" type="primary" onClick={onSubmit}>
|
||||
确定
|
||||
</Button>
|
||||
<Divider type="vertical" />
|
||||
</Space>
|
||||
</div>
|
||||
}
|
||||
title={intl.formatMessage({ id: props.title || 'access.cluster' })}
|
||||
visible={props.visible}
|
||||
placement="right"
|
||||
width={480}
|
||||
>
|
||||
<Spin spinning={loading || !!infoLoading}>
|
||||
<Form
|
||||
form={form}
|
||||
initialValues={{
|
||||
security,
|
||||
...clusterInfo,
|
||||
}}
|
||||
layout="vertical"
|
||||
onValuesChange={onHandleValuesChange}
|
||||
>
|
||||
<Form.Item
|
||||
name="name"
|
||||
label="集群名称"
|
||||
validateTrigger="onBlur"
|
||||
rules={[
|
||||
{
|
||||
required: true,
|
||||
validator: async (rule: any, value: string) => {
|
||||
if (!value) {
|
||||
return Promise.reject('集群名称不能为空');
|
||||
}
|
||||
|
||||
if (value === clusterInfo?.name) {
|
||||
return Promise.resolve();
|
||||
}
|
||||
|
||||
if (value?.length > 128) {
|
||||
return Promise.reject('集群名称长度限制在1~128字符');
|
||||
}
|
||||
if (!new RegExp(regClusterName).test(value)) {
|
||||
return Promise.reject(
|
||||
'集群名称支持中英文、数字、特殊字符 ! " # $ % & \' ( ) * + , - . / : ; < = > ? @ [ ] ^ _ ` { | } ~'
|
||||
);
|
||||
}
|
||||
return Utils.request(api.getClusterBasicExit(value)).then((res: any) => {
|
||||
const data = res || {};
|
||||
if (data?.exist) {
|
||||
return Promise.reject('集群名称重复');
|
||||
} else {
|
||||
return Promise.resolve();
|
||||
}
|
||||
});
|
||||
},
|
||||
},
|
||||
]}
|
||||
>
|
||||
<Input />
|
||||
</Form.Item>
|
||||
<Form.Item
|
||||
name="bootstrapServers"
|
||||
label="Bootstrap Servers"
|
||||
extra={
|
||||
extra.bootstrapExtra.includes('连接成功') ? (
|
||||
<span>{extra.bootstrapExtra}</span>
|
||||
) : (
|
||||
<span className="error-extra-info">{extra.bootstrapExtra}</span>
|
||||
)
|
||||
}
|
||||
validateTrigger={'onBlur'}
|
||||
rules={[
|
||||
{
|
||||
required: true,
|
||||
validator: async (rule: any, value: string) => {
|
||||
if (!value) {
|
||||
return Promise.reject('Bootstrap Servers不能为空');
|
||||
}
|
||||
if (value.length > 2000) {
|
||||
return Promise.reject('Bootstrap Servers长度限制在2000字符');
|
||||
}
|
||||
if (value && value !== lastFormItemValue.current.bootstrap) {
|
||||
return connectTest()
|
||||
.then((res: any) => {
|
||||
lastFormItemValue.current.bootstrap = value;
|
||||
|
||||
return Promise.resolve('');
|
||||
})
|
||||
.catch((err) => {
|
||||
return Promise.reject('连接失败');
|
||||
});
|
||||
}
|
||||
return Promise.resolve('');
|
||||
},
|
||||
},
|
||||
]}
|
||||
>
|
||||
<Input.TextArea
|
||||
rows={3}
|
||||
placeholder="请输入Bootstrap Servers地址,例如:192.168.1.1:9092,192.168.1.2:9092,192.168.1.3:9092"
|
||||
/>
|
||||
</Form.Item>
|
||||
<Form.Item
|
||||
name="zookeeper"
|
||||
label="Zookeeper"
|
||||
extra={
|
||||
extra.zooKeeperExtra.includes('连接成功') ? (
|
||||
<span>{extra.zooKeeperExtra}</span>
|
||||
) : (
|
||||
<span className="error-extra-info">{extra.zooKeeperExtra}</span>
|
||||
)
|
||||
}
|
||||
validateStatus={zookeeperErrorStatus ? 'error' : 'success'}
|
||||
validateTrigger={'onBlur'}
|
||||
rules={[
|
||||
{
|
||||
required: false,
|
||||
validator: async (rule: any, value: string) => {
|
||||
if (!value) {
|
||||
setZookeeperErrorStatus(false);
|
||||
return Promise.resolve('');
|
||||
}
|
||||
|
||||
if (value.length > 2000) {
|
||||
return Promise.reject('Zookeeper长度限制在2000字符');
|
||||
}
|
||||
|
||||
if (value && value !== lastFormItemValue.current.zookeeper) {
|
||||
return connectTest()
|
||||
.then((res: any) => {
|
||||
lastFormItemValue.current.zookeeper = value;
|
||||
setZookeeperErrorStatus(false);
|
||||
return Promise.resolve('');
|
||||
})
|
||||
.catch((err) => {
|
||||
setZookeeperErrorStatus(true);
|
||||
return Promise.reject('连接失败');
|
||||
});
|
||||
}
|
||||
return Promise.resolve('');
|
||||
},
|
||||
},
|
||||
]}
|
||||
>
|
||||
<Input.TextArea
|
||||
rows={3}
|
||||
placeholder="请输入Zookeeper地址,例如:192.168.0.1:2181,192.168.0.2:2181,192.168.0.2:2181/ks-kafka"
|
||||
/>
|
||||
</Form.Item>
|
||||
<Form.Item
|
||||
className="no-item-control"
|
||||
name="Metrics"
|
||||
label="Metrics"
|
||||
rules={[
|
||||
{
|
||||
required: false,
|
||||
message: '',
|
||||
},
|
||||
]}
|
||||
>
|
||||
<></>
|
||||
</Form.Item>
|
||||
<Form.Item
|
||||
name="jmxPort"
|
||||
label="JMX Port"
|
||||
className="inline-item adjust-height-style"
|
||||
extra={extra.jmxExtra}
|
||||
rules={[
|
||||
{
|
||||
required: false,
|
||||
message: '',
|
||||
},
|
||||
]}
|
||||
>
|
||||
<InputNumber style={{ width: 134 }} min={0} max={99999} />
|
||||
</Form.Item>
|
||||
<Form.Item
|
||||
name="maxConn"
|
||||
label="MaxConn"
|
||||
className="inline-item adjust-height-style"
|
||||
rules={[
|
||||
{
|
||||
required: false,
|
||||
message: '',
|
||||
},
|
||||
]}
|
||||
>
|
||||
<InputNumber style={{ width: 134 }} min={0} max={99999} />
|
||||
</Form.Item>
|
||||
<Form.Item
|
||||
name="security"
|
||||
label="Security"
|
||||
className="inline-item adjust-height-style"
|
||||
rules={[
|
||||
{
|
||||
required: false,
|
||||
message: '',
|
||||
},
|
||||
]}
|
||||
>
|
||||
<Radio.Group>
|
||||
<Radio value="None">None</Radio>
|
||||
<Radio value="Password">Password Authentication</Radio>
|
||||
</Radio.Group>
|
||||
</Form.Item>
|
||||
{security === 'Password' ? (
|
||||
<>
|
||||
<Form.Item
|
||||
className="inline-item max-width-66"
|
||||
name="username"
|
||||
label="User Info"
|
||||
style={{ width: '58%' }}
|
||||
rules={[
|
||||
{
|
||||
required: security === 'Password' || clusterInfo?.security === 'Password',
|
||||
validator: async (rule: any, value: string) => {
|
||||
if (!value) {
|
||||
return Promise.reject('用户名不能为空');
|
||||
}
|
||||
if (!new RegExp(regUsername).test(value)) {
|
||||
return Promise.reject('仅支持大小写、下划线、短划线(-)');
|
||||
}
|
||||
if (value.length > 128) {
|
||||
return Promise.reject('用户名长度限制在1~128字符');
|
||||
}
|
||||
return Promise.resolve();
|
||||
},
|
||||
},
|
||||
]}
|
||||
>
|
||||
<Input placeholder="请输入用户名" />
|
||||
</Form.Item>
|
||||
<Form.Item
|
||||
className="inline-item"
|
||||
name="token"
|
||||
label=""
|
||||
style={{ width: '38%', marginRight: 0 }}
|
||||
rules={[
|
||||
{
|
||||
required: security === 'Password' || clusterInfo?.security === 'Password',
|
||||
validator: async (rule: any, value: string) => {
|
||||
if (!value) {
|
||||
return Promise.reject('密码不能为空');
|
||||
}
|
||||
if (!new RegExp(regUsername).test(value)) {
|
||||
return Promise.reject('密码只能由大小写、下划线、短划线(-)组成');
|
||||
}
|
||||
if (value.length < 6 || value.length > 32) {
|
||||
return Promise.reject('密码长度限制在6~32字符');
|
||||
}
|
||||
return Promise.resolve();
|
||||
},
|
||||
},
|
||||
]}
|
||||
>
|
||||
<Input placeholder="请输入密码" />
|
||||
</Form.Item>
|
||||
</>
|
||||
) : null}
|
||||
<Form.Item
|
||||
name="kafkaVersion"
|
||||
label="Version"
|
||||
extra={<span className="error-extra-info">{extra.versionExtra}</span>}
|
||||
validateStatus={isLowVersion ? 'error' : 'success'}
|
||||
rules={[
|
||||
{
|
||||
required: true,
|
||||
validator: async (rule: any, value: any) => {
|
||||
if (!value) {
|
||||
setIsLowVersion(true);
|
||||
return Promise.reject('版本号不能为空');
|
||||
}
|
||||
// 检测版本号小于2.8.0,如果没有填zookeeper信息,才会提示
|
||||
const zookeeper = form.getFieldValue('zookeeper');
|
||||
if (value < lowKafkaVersion && !zookeeper) {
|
||||
setIsLowVersion(true);
|
||||
setExtra({
|
||||
...extra,
|
||||
versionExtra: intl.formatMessage({ id: 'access.cluster.low.version.tip' }),
|
||||
});
|
||||
return Promise.resolve();
|
||||
}
|
||||
setIsLowVersion(false);
|
||||
return Promise.resolve();
|
||||
},
|
||||
},
|
||||
]}
|
||||
>
|
||||
<Select placeholder="请选择Kafka Version,如无匹配则选择相近版本">
|
||||
{(props.kafkaVersion || []).map((item: string) => (
|
||||
<Select.Option key={item} value={item}>
|
||||
{item}
|
||||
</Select.Option>
|
||||
))}
|
||||
</Select>
|
||||
</Form.Item>
|
||||
|
||||
<Form.Item
|
||||
name="clientProperties"
|
||||
label="集群配置"
|
||||
rules={[
|
||||
{
|
||||
required: false,
|
||||
message: '请输入集群配置',
|
||||
},
|
||||
() => ({
|
||||
validator(_, value) {
|
||||
try {
|
||||
if (value) {
|
||||
JSON.parse(value);
|
||||
}
|
||||
|
||||
return Promise.resolve();
|
||||
} catch (e) {
|
||||
return Promise.reject(new Error('输入内容必须为 JSON'));
|
||||
}
|
||||
},
|
||||
}),
|
||||
]}
|
||||
>
|
||||
<div>
|
||||
<CodeMirrorFormItem
|
||||
resize
|
||||
defaultInput={form.getFieldValue('clientProperties')}
|
||||
placeholder={clientPropertiesPlaceholder}
|
||||
onBeforeChange={(clientProperties: string) => {
|
||||
form.setFieldsValue({ clientProperties });
|
||||
form.validateFields(['clientProperties']);
|
||||
}}
|
||||
onBlur={(value: any) => {
|
||||
form.validateFields(['clientProperties']).then(() => {
|
||||
const bootstrapServers = form.getFieldValue('bootstrapServers');
|
||||
const zookeeper = form.getFieldValue('zookeeper');
|
||||
const clientProperties = form.getFieldValue('clientProperties');
|
||||
|
||||
if (
|
||||
clientProperties &&
|
||||
clientProperties !== lastFormItemValue.current.clientProperties &&
|
||||
(!!bootstrapServers || !!zookeeper)
|
||||
) {
|
||||
connectTest()
|
||||
.then((res: any) => {
|
||||
lastFormItemValue.current.clientProperties = clientProperties;
|
||||
})
|
||||
.catch((err) => {
|
||||
message.error('连接失败');
|
||||
});
|
||||
}
|
||||
});
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
</Form.Item>
|
||||
<Form.Item
|
||||
name="description"
|
||||
label="集群描述"
|
||||
rules={[
|
||||
{
|
||||
required: false,
|
||||
validator: async (rule: any, value: string) => {
|
||||
if (!value) {
|
||||
return Promise.resolve('');
|
||||
}
|
||||
if (value && value.length > 200) {
|
||||
return Promise.reject('集群描述长度限制在200字符');
|
||||
}
|
||||
return Promise.resolve();
|
||||
},
|
||||
},
|
||||
]}
|
||||
>
|
||||
<Input.TextArea rows={rows} />
|
||||
</Form.Item>
|
||||
</Form>
|
||||
</Spin>
|
||||
</Drawer>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default AccessClusters;
|
||||
Reference in New Issue
Block a user