Files
KnowStreaming/km-console/packages/layout-clusters-fe/src/pages/MutliClusterPage/AccessCluster.tsx
2022-09-01 19:51:27 +08:00

628 lines
22 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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 '../../constants/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\\" pass
word=\\"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('集群名称长度限制在1128字符');
}
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('用户名长度限制在1128字符');
}
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('密码长度限制在632字符');
}
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;