refactor: 接入/编辑集群优化

This commit is contained in:
GraceWalk
2022-09-22 15:19:03 +08:00
parent 39015b5100
commit 08aa000c07
2 changed files with 279 additions and 368 deletions

View File

@@ -6,9 +6,8 @@ import { regClusterName, regUsername } from '@src/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客户端进行信息获取的相关配置
const LOW_KAFKA_VERSION = '2.8.0';
const CLIENT_PROPERTIES_PLACEHOLDER = `用于创建Kafka客户端进行信息获取的相关配置
例如开启SCRAM-SHA-256安全管控模式的集群需输入如下配置
未开启安全管控可不进行任何输入:
{
@@ -26,37 +25,25 @@ const AccessClusters = (props: any): JSX.Element => {
const intl = useIntl();
const [form] = Form.useForm();
const [loading, setLoading] = React.useState(false);
const [confirmLoading, setConfirmLoading] = React.useState(false);
const [curClusterInfo, setCurClusterInfo] = React.useState<any>({});
const [security, setSecurity] = React.useState(curClusterInfo?.security || 'None');
const [extra, setExtra] = React.useState({
versionExtra: '',
zooKeeperExtra: '',
bootstrapExtra: '',
jmxExtra: '',
});
const [isLowVersion, setIsLowVersion] = React.useState<boolean>(false);
const [zookeeperErrorStatus, setZookeeperErrorStatus] = React.useState<boolean>(false);
const lastFormItemValue = React.useRef({
bootstrap: curClusterInfo?.bootstrapServers || '',
bootstrapServers: curClusterInfo?.bootstrapServers || '',
zookeeper: curClusterInfo?.zookeeper || '',
clientProperties: curClusterInfo?.clientProperties || {},
});
const onHandleValuesChange = (value: any, allValues: any) => {
Object.keys(value).forEach((key) => {
const onHandleValuesChange = (changedValue: string[]) => {
Object.keys(changedValue).forEach((key) => {
switch (key) {
case 'security':
setSecurity(value.security);
break;
case 'zookeeper':
setExtra({
...extra,
zooKeeperExtra: '',
bootstrapExtra: '',
jmxExtra: '',
});
break;
case 'bootstrapServers':
setExtra({
...extra,
@@ -78,21 +65,19 @@ const AccessClusters = (props: any): JSX.Element => {
const onCancel = () => {
form.resetFields();
setLoading(false);
setZookeeperErrorStatus(false);
setIsLowVersion(false);
setSecurity('None');
setExtra({
versionExtra: '',
zooKeeperExtra: '',
bootstrapExtra: '',
jmxExtra: '',
});
lastFormItemValue.current = { bootstrap: '', zookeeper: '', clientProperties: {} };
lastFormItemValue.current = { bootstrapServers: '', zookeeper: '', clientProperties: {} };
props.setVisible && props.setVisible(false);
};
const onSubmit = () => {
form.validateFields().then((res) => {
setConfirmLoading(true);
let clientProperties = null;
try {
clientProperties = res.clientProperties && JSON.parse(res.clientProperties);
@@ -107,7 +92,7 @@ const AccessClusters = (props: any): JSX.Element => {
jmxProperties: {
jmxPort: res.jmxPort,
maxConn: res.maxConn,
openSSL: res.security === 'Password',
openSSL: res.openSSL || false,
token: res.token,
username: res.username,
},
@@ -115,7 +100,7 @@ const AccessClusters = (props: any): JSX.Element => {
name: res.name,
zookeeper: res.zookeeper || '',
};
setLoading(true);
if (!isNaN(curClusterInfo?.id)) {
Utils.put(api.phyCluster, {
...params,
@@ -127,7 +112,7 @@ const AccessClusters = (props: any): JSX.Element => {
onCancel();
})
.finally(() => {
setLoading(false);
setConfirmLoading(false);
});
} else {
Utils.post(api.phyCluster, params)
@@ -137,7 +122,7 @@ const AccessClusters = (props: any): JSX.Element => {
onCancel();
})
.finally(() => {
setLoading(false);
setConfirmLoading(false);
});
}
});
@@ -154,125 +139,224 @@ const AccessClusters = (props: any): JSX.Element => {
}
setLoading(true);
setIsLowVersion(false);
setZookeeperErrorStatus(false);
return Utils.post(api.kafkaValidator, {
bootstrapServers: bootstrapServers || '',
zookeeper: zookeeper || '',
clientProperties,
})
.then((res: any) => {
form.setFieldsValue({
jmxPort: res.jmxPort,
});
.then(
(res: {
errList: { code: number; message: string; data: any }[];
jmxPort: number | null;
kafkaVersion: string | null;
zookeeper: string | null;
}) => {
const changedValue: { jmxPort?: number; kafkaVersion?: string; zookeeper: string } = {
zookeeper: zookeeper || res.zookeeper,
};
if (res.kafkaVersion && props.kafkaVersion.includes(res.kafkaVersion)) {
changedValue.kafkaVersion = res.kafkaVersion;
}
if (res.jmxPort) {
changedValue.jmxPort = res.jmxPort;
}
form.setFieldsValue(changedValue);
if (props.kafkaVersion.indexOf(res.kafkaVersion) > -1) {
form.setFieldsValue({
kafkaVersion: res.kafkaVersion,
});
} else {
form.setFieldsValue({
kafkaVersion: undefined,
const extraMsg = {
...extra,
// 重置默认信息为连接成功
bootstrapExtra: bootstrapServers ? '连接成功' : '',
zooKeeperExtra: zookeeper ? '连接成功' : '',
};
const errList = res.errList || [];
// 处理错误信息
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;
}
});
setExtra(extraMsg);
return res;
}
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 = !(
curClusterInfo?.zookeeper ||
!curClusterInfo?.kafkaVersion ||
curClusterInfo?.kafkaVersion >= lowKafkaVersion
);
setIsLowVersion(showLowVersion);
setExtra({
...extraMsg,
versionExtra: showLowVersion ? intl.formatMessage({ id: 'access.cluster.low.version.tip' }) : '',
});
return res;
})
)
.finally(() => {
setLoading(false);
});
};
// 更新表单状态
React.useEffect(() => {
const showLowVersion = !(curClusterInfo?.zookeeper || !curClusterInfo?.kafkaVersion || curClusterInfo?.kafkaVersion >= lowKafkaVersion);
lastFormItemValue.current = {
bootstrap: curClusterInfo?.bootstrapServers || '',
bootstrapServers: curClusterInfo?.bootstrapServers || '',
zookeeper: curClusterInfo?.zookeeper || '',
clientProperties: curClusterInfo?.clientProperties || {},
};
setIsLowVersion(showLowVersion);
setExtra({
...extra,
versionExtra: showLowVersion ? intl.formatMessage({ id: 'access.cluster.low.version.tip' }) : '',
});
form.setFieldsValue({ ...curClusterInfo });
if (curClusterInfo?.kafkaVersion) {
form.validateFields(['kafkaVersion']);
}
}, [curClusterInfo]);
// 获取集群详情数据
React.useEffect(() => {
if (visible) {
if (clusterInfo?.id) {
setLoading(true);
const resolveJmxProperties = (obj: any) => {
const res = { ...obj };
try {
const originValue = obj?.jmxProperties;
if (originValue) {
const jmxProperties = JSON.parse(originValue);
typeof jmxProperties === 'object' && jmxProperties !== null && Object.assign(res, jmxProperties);
}
} catch (err) {
console.error('jmxProperties not JSON: ', err);
}
return res;
};
Utils.request(api.getPhyClusterBasic(clusterInfo.id))
.then((res: any) => {
let jmxProperties = null;
try {
jmxProperties = JSON.parse(res?.jmxProperties);
} catch (err) {
console.error(err);
}
// 转化值对应成表单值
if (jmxProperties?.openSSL) {
jmxProperties.security = 'Password';
}
if (jmxProperties) {
res = Object.assign({}, res || {}, jmxProperties);
}
setCurClusterInfo(res);
setLoading(false);
setCurClusterInfo(resolveJmxProperties(res));
})
.catch((err) => {
setCurClusterInfo(clusterInfo);
setCurClusterInfo(resolveJmxProperties(clusterInfo));
})
.finally(() => {
setLoading(false);
});
} else {
setCurClusterInfo(clusterInfo);
setCurClusterInfo({});
}
}
}, [visible, clusterInfo]);
const validators = {
name: async (_: any, value: string) => {
if (!value) {
return Promise.reject('集群名称不能为空');
}
if (value === curClusterInfo?.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 || {};
return data?.exist ? Promise.reject('集群名称重复') : Promise.resolve();
})
.catch(() => Promise.reject('连接超时! 请重试或检查服务'));
},
bootstrapServers: async (_: 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.bootstrapServers) {
lastFormItemValue.current.bootstrapServers = value;
return connectTest().catch(() => (lastFormItemValue.current.bootstrapServers = ''));
}
return Promise.resolve('');
},
zookeeper: async (_: any, value: string) => {
if (!value) {
return Promise.resolve('');
}
if (value.length > 2000) {
return Promise.reject('Zookeeper长度限制在2000字符');
}
if (value && value !== lastFormItemValue.current.zookeeper) {
lastFormItemValue.current.zookeeper = value;
return connectTest().catch(() => (lastFormItemValue.current.zookeeper = ''));
}
return Promise.resolve('');
},
securityUserName: async (_: 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();
},
securityToken: async (_: 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();
},
kafkaVersion: async (_: any, value: any) => {
if (!value) {
return Promise.reject('版本号不能为空');
}
// 检测版本号小于2.8.0如果没有填zookeeper信息才会提示
const zookeeper = form.getFieldValue('zookeeper');
let versionExtra = '';
if (value < LOW_KAFKA_VERSION && !zookeeper) {
versionExtra = intl.formatMessage({ id: 'access.cluster.low.version.tip' });
}
setExtra({
...extra,
versionExtra,
});
return Promise.resolve();
},
clientProperties: async (_: any, value: string) => {
try {
if (value) {
JSON.parse(value);
}
return Promise.resolve();
} catch (e) {
return Promise.reject(new Error('输入内容必须为 JSON'));
}
},
description: async (_: any, value: string) => {
if (!value) {
return Promise.resolve('');
}
if (value && value.length > 200) {
return Promise.reject('集群描述长度限制在200字符');
}
return Promise.resolve();
},
};
return (
<>
<Drawer
@@ -285,14 +369,14 @@ const AccessClusters = (props: any): JSX.Element => {
<Button size="small" onClick={onCancel}>
</Button>
<Button size="small" type="primary" onClick={onSubmit}>
<Button size="small" type="primary" loading={confirmLoading} onClick={onSubmit}>
</Button>
<Divider type="vertical" />
</Space>
</div>
}
title={intl.formatMessage({ id: props.title || 'access.cluster' })}
title={intl.formatMessage({ id: props.title || clusterInfo?.id ? 'edit.cluster' : 'access.cluster' })}
visible={props.visible}
placement="right"
width={480}
@@ -306,30 +390,7 @@ const AccessClusters = (props: any): JSX.Element => {
rules={[
{
required: true,
validator: async (rule: any, value: string) => {
if (!value) {
return Promise.reject('集群名称不能为空');
}
if (value === curClusterInfo?.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();
}
});
},
validator: validators.name,
},
]}
>
@@ -343,26 +404,7 @@ const AccessClusters = (props: any): JSX.Element => {
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('');
},
validator: validators.bootstrapServers,
},
]}
>
@@ -375,35 +417,10 @@ const AccessClusters = (props: any): JSX.Element => {
name="zookeeper"
label="Zookeeper"
extra={<span className={!extra.zooKeeperExtra.includes('连接成功') ? '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('');
},
validator: validators.zookeeper,
},
]}
>
@@ -412,142 +429,65 @@ const AccessClusters = (props: any): JSX.Element => {
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' || curClusterInfo?.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 className="metrics-form-item" label="Metrics">
<div className="horizontal-form-container">
<div className="inline-items">
<Form.Item name="jmxPort" label="JMX Port :" extra={extra.jmxExtra}>
<InputNumber min={0} max={99999} style={{ width: 129 }} />
</Form.Item>
<Form.Item name="maxConn" label="Max Conn :">
<InputNumber addonAfter="个" min={0} max={99999} style={{ width: 124 }} />
</Form.Item>
</div>
<Form.Item name="openSSL" label="Security :">
<Radio.Group>
<Radio value={false}>None</Radio>
<Radio value={true}>Password Authentication</Radio>
</Radio.Group>
</Form.Item>
<Form.Item
className="inline-item"
name="token"
label=""
style={{ width: '38%', marginRight: 0 }}
rules={[
{
required: security === 'Password' || curClusterInfo?.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 dependencies={['openSSL']} noStyle>
{({ getFieldValue }) => {
return getFieldValue('openSSL') ? (
<div className="user-info-form-items">
<Form.Item className="user-info-label" label="User Info :" required />
<div className="inline-items">
<Form.Item
name="username"
rules={[
{
validator: validators.securityUserName,
},
]}
>
<Input placeholder="请输入用户名" />
</Form.Item>
<Form.Item
className="token-form-item"
name="token"
rules={[
{
validator: validators.securityToken,
},
]}
>
<Input placeholder="请输入密码" />
</Form.Item>
</div>
</div>
) : null;
}}
</Form.Item>
</>
) : null}
</div>
</Form.Item>
<Form.Item
name="kafkaVersion"
label="Version"
dependencies={['zookeeper']}
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();
},
validator: validators.kafkaVersion,
},
]}
>
@@ -565,29 +505,15 @@ const AccessClusters = (props: any): JSX.Element => {
label="集群配置"
rules={[
{
required: false,
message: '请输入集群配置',
validator: validators.clientProperties,
},
() => ({
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}
placeholder={CLIENT_PROPERTIES_PLACEHOLDER}
onBeforeChange={(clientProperties: string) => {
form.setFieldsValue({ clientProperties });
form.validateFields(['clientProperties']);
@@ -621,20 +547,11 @@ const AccessClusters = (props: any): JSX.Element => {
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();
},
validator: validators.description,
},
]}
>
<Input.TextArea rows={rows} />
<Input.TextArea rows={4} />
</Form.Item>
</Form>
</Spin>

View File

@@ -656,43 +656,37 @@
color: @error-color;
}
}
.inline-item.dcloud-form-item {
display: -webkit-inline-box;
margin-right: 16px;
&.adjust-height-style {
.dcloud-form-item-label {
padding: 0;
label {
height: 36px;
}
}
.dcloud-form-item-control {
&-input {
height: 36px;
}
}
.horizontal-form-container {
padding-left: 16px;
.inline-items {
display: flex;
justify-content: space-between;
}
&.max-width-66 {
.dcloud-form-item-control {
max-width: 66%;
}
}
.dcloud-form-item-label {
margin-right: 12px;
label {
.dcloud-form-item {
flex-direction: row;
align-items: center;
&-label {
padding: 0 12px 0 0;
font-size: 13px;
font-family: @font-family;
color: #74788d;
}
}
}
.no-item-control {
margin-bottom: 8px !important;
.dcloud-form-item-control {
display: none;
.metrics-form-item {
margin-top: 8px;
}
.user-info-form-items {
display: flex;
align-items: flex-start;
.user-info-label {
padding-top: 4px;
}
.inline-items {
flex: 0 0 80%;
.token-form-item {
margin-left: 16px;
}
}
}
}
}