mirror of
https://github.com/didi/KnowStreaming.git
synced 2026-01-14 12:02:13 +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;
|
||||
@@ -0,0 +1,130 @@
|
||||
import { DoubleRightOutlined } from '@ant-design/icons';
|
||||
import { Checkbox } from 'knowdesign';
|
||||
import { debounce } from 'lodash';
|
||||
import React, { useEffect } from 'react';
|
||||
|
||||
const CheckboxGroup = Checkbox.Group;
|
||||
|
||||
interface IVersion {
|
||||
firstLine: string[];
|
||||
leftVersion: string[];
|
||||
}
|
||||
|
||||
const CustomCheckGroup = (props: { kafkaVersion: string[]; onChangeCheckGroup: any }) => {
|
||||
const { kafkaVersion, onChangeCheckGroup } = props;
|
||||
const [checkedKafkaVersion, setCheckedKafkaVersion] = React.useState<IVersion>({
|
||||
firstLine: [],
|
||||
leftVersion: [],
|
||||
});
|
||||
const [allVersion, setAllVersion] = React.useState<IVersion>({
|
||||
firstLine: [],
|
||||
leftVersion: [],
|
||||
});
|
||||
|
||||
const [indeterminate, setIndeterminate] = React.useState(false);
|
||||
const [checkAll, setCheckAll] = React.useState(true);
|
||||
const [moreGroupWidth, setMoreGroupWidth] = React.useState(400);
|
||||
const [showMore, setShowMore] = React.useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
document.addEventListener('click', handleDocumentClick);
|
||||
return () => {
|
||||
document.removeEventListener('click', handleDocumentClick);
|
||||
}
|
||||
}, []);
|
||||
|
||||
const handleDocumentClick = (e: Event) => {
|
||||
setShowMore(false);
|
||||
}
|
||||
|
||||
const setCheckAllStauts = (list: string[], otherList: string[]) => {
|
||||
onChangeCheckGroup([...list, ...otherList]);
|
||||
setIndeterminate(!!list.length && list.length + otherList.length < kafkaVersion.length);
|
||||
setCheckAll(list.length + otherList.length === kafkaVersion.length);
|
||||
};
|
||||
|
||||
const getTwoPanelVersion = () => {
|
||||
const width = (document.getElementsByClassName('custom-check-group')[0] as any)?.offsetWidth;
|
||||
const checkgroupWidth = width - 100 - 86;
|
||||
const num = (checkgroupWidth / 108) | 0;
|
||||
const firstLine = Array.from(kafkaVersion).splice(0, num);
|
||||
setMoreGroupWidth(num * 108 + 88 + 66);
|
||||
const leftVersion = Array.from(kafkaVersion).splice(num);
|
||||
return { firstLine, leftVersion };
|
||||
};
|
||||
|
||||
const onFirstVersionChange = (list: []) => {
|
||||
setCheckedKafkaVersion({
|
||||
...checkedKafkaVersion,
|
||||
firstLine: list,
|
||||
});
|
||||
|
||||
setCheckAllStauts(list, checkedKafkaVersion.leftVersion);
|
||||
};
|
||||
|
||||
const onLeftVersionChange = (list: []) => {
|
||||
setCheckedKafkaVersion({
|
||||
...checkedKafkaVersion,
|
||||
leftVersion: list,
|
||||
});
|
||||
setCheckAllStauts(list, checkedKafkaVersion.firstLine);
|
||||
};
|
||||
|
||||
const onCheckAllChange = (e: any) => {
|
||||
const versions = getTwoPanelVersion();
|
||||
|
||||
setCheckedKafkaVersion(
|
||||
e.target.checked
|
||||
? versions
|
||||
: {
|
||||
firstLine: [],
|
||||
leftVersion: [],
|
||||
}
|
||||
);
|
||||
onChangeCheckGroup(e.target.checked ? [...versions.firstLine, ...versions.leftVersion] : []);
|
||||
|
||||
setIndeterminate(false);
|
||||
setCheckAll(e.target.checked);
|
||||
};
|
||||
|
||||
React.useEffect(() => {
|
||||
const handleVersionLine = () => {
|
||||
const versions = getTwoPanelVersion();
|
||||
setAllVersion(versions);
|
||||
setCheckedKafkaVersion(versions);
|
||||
};
|
||||
handleVersionLine();
|
||||
|
||||
window.addEventListener('resize', handleVersionLine); //监听窗口大小改变
|
||||
return () => window.removeEventListener('resize', debounce(handleVersionLine, 500));
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className="custom-check-group" onClick={(e) => e.nativeEvent.stopImmediatePropagation()}>
|
||||
<div>
|
||||
<Checkbox className="check-all" indeterminate={indeterminate} onChange={onCheckAllChange} checked={checkAll}>
|
||||
全选
|
||||
</Checkbox>
|
||||
</div>
|
||||
<CheckboxGroup options={allVersion.firstLine} value={checkedKafkaVersion.firstLine} onChange={onFirstVersionChange} />
|
||||
{showMore ? (
|
||||
<CheckboxGroup
|
||||
style={{ width: moreGroupWidth }}
|
||||
className="more-check-group"
|
||||
options={allVersion.leftVersion}
|
||||
value={checkedKafkaVersion.leftVersion}
|
||||
onChange={onLeftVersionChange}
|
||||
/>
|
||||
) : null}
|
||||
<div className="more-btn" onClick={() => setShowMore(!showMore)}>
|
||||
<a>
|
||||
{!showMore ? '展开更多' : '收起更多'} <DoubleRightOutlined style={{ transform: `rotate(${showMore ? '270' : '90'}deg)` }} />
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default CustomCheckGroup;
|
||||
@@ -0,0 +1,317 @@
|
||||
import React, { useEffect, useMemo, useRef, useState, useReducer } from 'react';
|
||||
import { Slider, Input, Select, Checkbox, Button, Utils, Spin, IconFont, AppContainer } from 'knowdesign';
|
||||
import API from '../../api';
|
||||
import TourGuide, { MultiPageSteps } from '@src/components/TourGuide';
|
||||
import './index.less';
|
||||
import { healthSorceList, linesMetric, pointsMetric, sortFieldList, sortTypes, statusFilters } from './config';
|
||||
import { oneDayMillims } from '../../constants/common';
|
||||
import ListScroll from './List';
|
||||
import AccessClusters from './AccessCluster';
|
||||
import CustomCheckGroup from './CustomCheckGroup';
|
||||
import { ClustersPermissionMap } from '../CommonConfig';
|
||||
|
||||
const CheckboxGroup = Checkbox.Group;
|
||||
const { Option } = Select;
|
||||
|
||||
const MultiClusterPage = () => {
|
||||
const [run, setRun] = useState<boolean>(false);
|
||||
const [global] = AppContainer.useGlobalValue();
|
||||
const [statusList, setStatusList] = React.useState([1, 0]);
|
||||
const [kafkaVersion, setKafkaVersion] = React.useState({});
|
||||
const [visible, setVisible] = React.useState(false);
|
||||
const [list, setList] = useState<[]>([]);
|
||||
const [healthScoreRange, setHealthScoreRange] = React.useState([0, 100]);
|
||||
const [checkedKafkaVersion, setCheckedKafkaVersion] = React.useState({});
|
||||
const [sortInfo, setSortInfo] = React.useState({
|
||||
sortField: 'HealthScore',
|
||||
sortType: 'asc',
|
||||
});
|
||||
const [clusterLoading, setClusterLoading] = useState(true);
|
||||
const [pageLoading, setPageLoading] = useState(true);
|
||||
const [isReload, setIsReload] = useState(false);
|
||||
const [versionLoading, setVersionLoading] = useState(true);
|
||||
const [searchKeywords, setSearchKeywords] = useState('');
|
||||
const [stateInfo, setStateInfo] = React.useState({
|
||||
downCount: 0,
|
||||
liveCount: 0,
|
||||
total: 0,
|
||||
});
|
||||
const [pagination, setPagination] = useState({
|
||||
pageNo: 1,
|
||||
pageSize: 10,
|
||||
total: 0,
|
||||
});
|
||||
|
||||
const searchKeyword = useRef('');
|
||||
|
||||
const getPhyClustersDashbord = (pageNo: number, pageSize: number) => {
|
||||
const endTime = new Date().getTime();
|
||||
const startTime = endTime - oneDayMillims;
|
||||
const params = {
|
||||
metricLines: {
|
||||
endTime,
|
||||
metricsNames: linesMetric,
|
||||
startTime,
|
||||
},
|
||||
latestMetricNames: pointsMetric,
|
||||
pageNo: pageNo || 1,
|
||||
pageSize: pageSize || 10,
|
||||
preciseFilterDTOList: [
|
||||
{
|
||||
fieldName: 'kafkaVersion',
|
||||
fieldValueList: checkedKafkaVersion,
|
||||
},
|
||||
],
|
||||
rangeFilterDTOList: [
|
||||
{
|
||||
fieldMaxValue: healthScoreRange[1],
|
||||
fieldMinValue: healthScoreRange[0],
|
||||
fieldName: 'HealthScore',
|
||||
},
|
||||
],
|
||||
searchKeywords,
|
||||
...sortInfo,
|
||||
};
|
||||
|
||||
if (statusList.length === 1) {
|
||||
params.preciseFilterDTOList.push({
|
||||
fieldName: 'Alive',
|
||||
fieldValueList: statusList,
|
||||
});
|
||||
}
|
||||
return Utils.post(API.phyClustersDashbord, params);
|
||||
};
|
||||
|
||||
const getSupportKafkaVersion = () => {
|
||||
setVersionLoading(true);
|
||||
Utils.request(API.supportKafkaVersion)
|
||||
.then((res) => {
|
||||
setKafkaVersion(res || {});
|
||||
setVersionLoading(false);
|
||||
setCheckedKafkaVersion(res ? Object.keys(res) : []);
|
||||
})
|
||||
.catch((err) => {
|
||||
setVersionLoading(false);
|
||||
});
|
||||
};
|
||||
|
||||
const getPhyClusterState = () => {
|
||||
Utils.request(API.phyClusterState)
|
||||
.then((res: any) => {
|
||||
setStateInfo(res);
|
||||
})
|
||||
.finally(() => {
|
||||
setPageLoading(false);
|
||||
});
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
getPhyClusterState();
|
||||
getSupportKafkaVersion();
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
if (!pageLoading && stateInfo.total) {
|
||||
setRun(true);
|
||||
}
|
||||
}, [pageLoading, stateInfo]);
|
||||
|
||||
useEffect(() => {
|
||||
if (versionLoading) return;
|
||||
setClusterLoading(true);
|
||||
getPhyClustersDashbord(pagination.pageNo, pagination.pageSize)
|
||||
.then((res: any) => {
|
||||
setPagination(res.pagination);
|
||||
setList(res?.bizData || []);
|
||||
return res;
|
||||
})
|
||||
.finally(() => {
|
||||
setClusterLoading(false);
|
||||
});
|
||||
}, [sortInfo, checkedKafkaVersion, healthScoreRange, statusList, searchKeywords, isReload]);
|
||||
|
||||
const onSilderChange = (value: number[]) => {
|
||||
setHealthScoreRange(value);
|
||||
};
|
||||
|
||||
const onSelectChange = (type: string, value: string) => {
|
||||
setSortInfo({
|
||||
...sortInfo,
|
||||
[type]: value,
|
||||
});
|
||||
};
|
||||
|
||||
const onStatusChange = (list: []) => {
|
||||
setStatusList(list);
|
||||
};
|
||||
|
||||
const onInputChange = (e: any) => {
|
||||
const { value } = e.target;
|
||||
setSearchKeywords(value.trim());
|
||||
};
|
||||
|
||||
const onChangeCheckGroup = (list: []) => {
|
||||
setCheckedKafkaVersion(list);
|
||||
};
|
||||
|
||||
const afterSubmitSuccessAccessClusters = () => {
|
||||
getPhyClusterState();
|
||||
setIsReload(!isReload);
|
||||
};
|
||||
|
||||
const renderEmpty = () => {
|
||||
return (
|
||||
<div className="empty-page">
|
||||
<div className="title">Kafka 多集群管理</div>
|
||||
<div className="img">
|
||||
<div className="img-card-1" />
|
||||
<div className="img-card-2" />
|
||||
<div className="img-card-3" />
|
||||
</div>
|
||||
<div>
|
||||
<Button className="header-filter-top-button" type="primary" onClick={() => setVisible(true)}>
|
||||
<span>
|
||||
<IconFont type="icon-jiahao" />
|
||||
<span className="text">接入集群</span>
|
||||
</span>
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
const renderLoading = () => {
|
||||
return (
|
||||
<div style={{ height: '100%', display: 'flex', justifyContent: 'center', alignItems: 'center' }}>
|
||||
<Spin spinning={true} />
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
const renderContent = () => {
|
||||
return (
|
||||
<div className="multi-cluster-page" id="scrollableDiv">
|
||||
<div className="multi-cluster-page-fixed">
|
||||
<div className="content-container">
|
||||
<div className="multi-cluster-header">
|
||||
<div className="cluster-header-card">
|
||||
<div className="cluster-header-card-bg-left"></div>
|
||||
<div className="cluster-header-card-bg-right"></div>
|
||||
<h5 className="header-card-title">
|
||||
Clusters<span className="chinese-text"> 总数</span>
|
||||
</h5>
|
||||
<div className="header-card-total">{stateInfo.total}</div>
|
||||
<div className="header-card-info">
|
||||
<div className="card-info-item card-info-item-live">
|
||||
<div>
|
||||
live
|
||||
<span className="info-item-value">
|
||||
<em>{stateInfo.liveCount}</em>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
<div className="card-info-item card-info-item-down">
|
||||
<div>
|
||||
down
|
||||
<span className="info-item-value">
|
||||
<em>{stateInfo.downCount}</em>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="cluster-header-filter">
|
||||
<div className="header-filter-top">
|
||||
<div className="header-filter-top-input">
|
||||
<Input
|
||||
onPressEnter={onInputChange}
|
||||
onChange={(e) => (searchKeyword.current = e.target.value)}
|
||||
allowClear
|
||||
bordered={false}
|
||||
placeholder="请输入ClusterName进行搜索"
|
||||
suffix={<IconFont className="icon" type="icon-fangdajing" onClick={() => setSearchKeywords(searchKeyword.current)} />}
|
||||
/>
|
||||
</div>
|
||||
{global.hasPermission && global.hasPermission(ClustersPermissionMap.CLUSTER_ADD) ? (
|
||||
<>
|
||||
<div className="header-filter-top-divider"></div>
|
||||
<Button className="header-filter-top-button" type="primary" onClick={() => setVisible(true)}>
|
||||
<IconFont type="icon-jiahao" />
|
||||
<span className="text">接入集群</span>
|
||||
</Button>
|
||||
</>
|
||||
) : (
|
||||
<></>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className="header-filter-bottom">
|
||||
<div className="header-filter-bottom-item header-filter-bottom-item-checkbox">
|
||||
<h3 className="header-filter-bottom-item-title">版本选择</h3>
|
||||
<div className="header-filter-bottom-item-content flex">
|
||||
{Object.keys(kafkaVersion).length ? (
|
||||
<CustomCheckGroup kafkaVersion={Object.keys(kafkaVersion)} onChangeCheckGroup={onChangeCheckGroup} />
|
||||
) : null}
|
||||
</div>
|
||||
</div>
|
||||
<div className="header-filter-bottom-item header-filter-bottom-item-slider">
|
||||
<h3 className="header-filter-bottom-item-title title-right">健康分</h3>
|
||||
<div className="header-filter-bottom-item-content">
|
||||
<Slider range step={20} defaultValue={[0, 100]} marks={healthSorceList} onAfterChange={onSilderChange} />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="multi-cluster-filter">
|
||||
<div className="multi-cluster-filter-select">
|
||||
<Select
|
||||
onChange={(value) => onSelectChange('sortField', value)}
|
||||
defaultValue="HealthScore"
|
||||
style={{ width: 170, marginRight: 12 }}
|
||||
>
|
||||
{sortFieldList.map((item) => (
|
||||
<Option key={item.value} value={item.value}>
|
||||
{item.label}
|
||||
</Option>
|
||||
))}
|
||||
</Select>
|
||||
<Select onChange={(value) => onSelectChange('sortType', value)} defaultValue="asc" style={{ width: 170 }}>
|
||||
{sortTypes.map((item) => (
|
||||
<Option key={item.value} value={item.value}>
|
||||
{item.label}
|
||||
</Option>
|
||||
))}
|
||||
</Select>
|
||||
</div>
|
||||
<div className="multi-cluster-filter-checkbox">
|
||||
<CheckboxGroup options={statusFilters} value={statusList} onChange={onStatusChange} />
|
||||
</div>
|
||||
</div>
|
||||
<div className="test-modal-23"></div>
|
||||
</div>
|
||||
</div>
|
||||
<Spin spinning={clusterLoading}>{renderList}</Spin>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
const renderList = useMemo(() => {
|
||||
return <ListScroll list={list} pagination={pagination} loadMoreData={getPhyClustersDashbord} getPhyClusterState={getPhyClusterState} />;
|
||||
}, [list, pagination]);
|
||||
|
||||
return (
|
||||
<>
|
||||
<TourGuide guide={MultiPageSteps} run={run} />
|
||||
{pageLoading ? renderLoading() : stateInfo.total ? renderContent() : renderEmpty()}
|
||||
<AccessClusters
|
||||
visible={visible}
|
||||
setVisible={setVisible}
|
||||
kafkaVersion={Object.keys(kafkaVersion)}
|
||||
afterSubmitSuccess={afterSubmitSuccessAccessClusters}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default MultiClusterPage;
|
||||
@@ -0,0 +1,373 @@
|
||||
import { AppContainer, Divider, Form, IconFont, Input, List, message, Modal, Progress, Spin, Tooltip, Utils } from 'knowdesign';
|
||||
import moment from 'moment';
|
||||
import React, { useEffect, useMemo, useState, useReducer } from 'react';
|
||||
import InfiniteScroll from 'react-infinite-scroll-component';
|
||||
import { Link, useHistory } from 'react-router-dom';
|
||||
import { timeFormat } from '../../constants/common';
|
||||
import { IMetricPoint, linesMetric } from './config';
|
||||
import { useIntl } from 'react-intl';
|
||||
import api, { MetricType } from '../../api';
|
||||
import { getHealthClassName, getHealthProcessColor, getHealthText } from '../SingleClusterDetail/config';
|
||||
import { ClustersPermissionMap } from '../CommonConfig';
|
||||
import { getUnit, getDataNumberUnit } from '@src/constants/chartConfig';
|
||||
import SmallChart from '@src/components/SmallChart';
|
||||
|
||||
const ListScroll = (props: { loadMoreData: any; list: any; pagination: any; getPhyClusterState: any }) => {
|
||||
const history = useHistory();
|
||||
const [global] = AppContainer.useGlobalValue();
|
||||
const [form] = Form.useForm();
|
||||
const [list, setList] = useState<[]>(props.list || []);
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [visible, setVisible] = useState(false);
|
||||
const [clusterInfo, setClusterInfo] = useState({} as any);
|
||||
const [pagination, setPagination] = useState(
|
||||
props.pagination || {
|
||||
pageNo: 1,
|
||||
pageSize: 10,
|
||||
total: 0,
|
||||
}
|
||||
);
|
||||
const intl = useIntl();
|
||||
|
||||
useEffect(() => {
|
||||
setList(props.list || []);
|
||||
setPagination(props.pagination || {});
|
||||
}, [props.list, props.pagination]);
|
||||
|
||||
useEffect(() => {
|
||||
if (visible) {
|
||||
form.resetFields();
|
||||
}
|
||||
}, [visible]);
|
||||
|
||||
const loadMoreData = async () => {
|
||||
if (loading) {
|
||||
return;
|
||||
}
|
||||
setLoading(true);
|
||||
|
||||
const res = await props.loadMoreData(pagination.pageNo + 1, pagination.pageSize);
|
||||
const _data = list.concat(res.bizData || []) as any;
|
||||
setList(_data);
|
||||
setPagination(res.pagination);
|
||||
setLoading(false);
|
||||
};
|
||||
|
||||
const RenderItem = (itemData: any) => {
|
||||
itemData = itemData || {};
|
||||
const metrics = linesMetric;
|
||||
const metricPoints = [] as IMetricPoint[];
|
||||
metrics.forEach((item) => {
|
||||
const line = {
|
||||
metricName: item,
|
||||
value: itemData.latestMetrics?.metrics?.[item] || 0,
|
||||
unit: (global.getMetricDefine && global.getMetricDefine(MetricType.Cluster, item).unit) || '',
|
||||
metricLines: {
|
||||
name: item,
|
||||
data: itemData.metricLines
|
||||
.find((metric: any) => metric.metricName === item)
|
||||
?.metricPoints.map((point: IMetricPoint) => [point.timeStamp, point.value]),
|
||||
},
|
||||
} as IMetricPoint;
|
||||
|
||||
// 如果单位是 字节 ,进行单位换算
|
||||
if (line.unit.toLowerCase().includes('byte')) {
|
||||
const [unit, size] = getUnit(line.value);
|
||||
line.value = Number((line.value / size).toFixed(2));
|
||||
line.unit = line.unit.toLowerCase().replace('byte', unit);
|
||||
}
|
||||
|
||||
// Messages 指标值特殊处理
|
||||
if (line.metricName === 'LeaderMessages') {
|
||||
const [unit, size] = getDataNumberUnit(line.value);
|
||||
line.value = Number((line.value / size).toFixed(2));
|
||||
line.unit = unit + line.unit;
|
||||
}
|
||||
|
||||
metricPoints.push(line);
|
||||
});
|
||||
|
||||
const {
|
||||
Brokers: brokers,
|
||||
Zookeepers: zks,
|
||||
HealthCheckPassed: healthCheckPassed,
|
||||
HealthCheckTotal: healthCheckTotal,
|
||||
HealthScore: healthScore,
|
||||
ZookeepersAvailable: zookeepersAvailable,
|
||||
LoadReBalanceCpu: loadReBalanceCpu,
|
||||
LoadReBalanceDisk: loadReBalanceDisk,
|
||||
LoadReBalanceEnable: loadReBalanceEnable,
|
||||
LoadReBalanceNwIn: loadReBalanceNwIn,
|
||||
LoadReBalanceNwOut: loadReBalanceNwOut,
|
||||
} = itemData.latestMetrics?.metrics || {};
|
||||
|
||||
return (
|
||||
<List.Item
|
||||
onClick={() => {
|
||||
history.push(`/cluster/${itemData.id}/cluster`);
|
||||
}}
|
||||
>
|
||||
<div className={'multi-cluster-list-item'}>
|
||||
<div className="multi-cluster-list-item-healthy">
|
||||
<Progress
|
||||
type="circle"
|
||||
status={!itemData.alive ? 'exception' : healthScore >= 90 ? 'success' : 'normal'}
|
||||
strokeWidth={4}
|
||||
// className={healthScore > 90 ? 'green-circle' : ''}
|
||||
className={+itemData.alive <= 0 ? 'red-circle' : +healthScore < 90 ? 'blue-circle' : 'green-circle'}
|
||||
strokeColor={getHealthProcessColor(healthScore, itemData.alive)}
|
||||
percent={itemData.alive ? healthScore : 100}
|
||||
format={() => (
|
||||
<div className={`healthy-percent ${getHealthClassName(healthScore, itemData?.alive)}`}>
|
||||
{getHealthText(healthScore, itemData?.alive)}
|
||||
</div>
|
||||
)}
|
||||
width={70}
|
||||
/>
|
||||
<div className="healthy-degree">
|
||||
<span className="healthy-degree-status">通过</span>
|
||||
<span className="healthy-degree-proportion">
|
||||
{healthCheckPassed}/{healthCheckTotal}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
<div className="multi-cluster-list-item-right">
|
||||
<div className="multi-cluster-list-item-base">
|
||||
<div className="multi-cluster-list-item-base-left">
|
||||
<div className="base-name">{itemData.name ?? '-'}</div>
|
||||
<span className="base-version">{itemData.kafkaVersion ?? '-'}</span>
|
||||
{loadReBalanceEnable !== undefined && (
|
||||
<div
|
||||
style={{
|
||||
display: 'flex',
|
||||
}}
|
||||
onClick={(e) => {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
}}
|
||||
>
|
||||
{[
|
||||
['BytesIn', loadReBalanceEnable && loadReBalanceNwIn],
|
||||
['BytesOut', loadReBalanceEnable && loadReBalanceNwOut],
|
||||
['Disk', loadReBalanceEnable && loadReBalanceDisk],
|
||||
].map(([name, isBalanced]) => {
|
||||
return isBalanced ? (
|
||||
<div className="balance-box balanced">{name} 已均衡</div>
|
||||
) : loadReBalanceEnable ? (
|
||||
<div className="balance-box unbalanced">{name} 未均衡</div>
|
||||
) : (
|
||||
<Tooltip
|
||||
title={
|
||||
<span>
|
||||
尚未开启 {name} 均衡策略,
|
||||
<Link to={`/cluster/${itemData.id}/cluster/balance`}>前往开启</Link>
|
||||
</span>
|
||||
}
|
||||
>
|
||||
<div className="balance-box unbalanced">{name} 未均衡</div>
|
||||
</Tooltip>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
<div className="multi-cluster-list-item-base-date">{moment(itemData.createTime).format(timeFormat)}</div>
|
||||
</div>
|
||||
<div className="multi-cluster-list-item-Indicator">
|
||||
<div className="indicator-left">
|
||||
<div className="indicator-left-item">
|
||||
<div className="indicator-left-item-title">
|
||||
<span
|
||||
className="indicator-left-item-title-dot"
|
||||
style={{
|
||||
background: itemData.latestMetrics?.metrics?.BrokersNotAlive ? '#FF7066' : '#34C38F',
|
||||
}}
|
||||
></span>
|
||||
Brokers
|
||||
</div>
|
||||
<div className="indicator-left-item-value">{brokers}</div>
|
||||
</div>
|
||||
<div className="indicator-left-item">
|
||||
<div className="indicator-left-item-title">
|
||||
<span
|
||||
className="indicator-left-item-title-dot"
|
||||
style={{
|
||||
background: zookeepersAvailable === -1 ? '#e9e7e7' : zookeepersAvailable === 0 ? '#FF7066' : '#34C38F',
|
||||
}}
|
||||
></span>
|
||||
ZK
|
||||
</div>
|
||||
<div className="indicator-left-item-value">{zookeepersAvailable === -1 ? '-' : zks}</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="indicator-right">
|
||||
{metricPoints.map((row, index) => {
|
||||
return (
|
||||
<div
|
||||
key={row.metricName + index}
|
||||
className={`indicator-right-item ${row.metricName === 'LeaderMessages' ? 'first-right-item' : ''}`}
|
||||
>
|
||||
<div className="indicator-right-item-total">
|
||||
<div className="indicator-right-item-total-name">
|
||||
{row.metricName === 'TotalLogSize' ? 'MessageSize' : row.metricName}
|
||||
</div>
|
||||
<div className="indicator-right-item-total-value">
|
||||
{row.value}
|
||||
<span className="total-value-unit">{row.unit}</span>
|
||||
</div>
|
||||
</div>
|
||||
<div className="indicator-right-item-chart">
|
||||
<SmallChart width={79} height={40} chartData={row.metricLines} />
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{global.hasPermission && global.hasPermission(ClustersPermissionMap.CLUSTER_DEL) ? (
|
||||
<div className="multi-cluster-list-item-btn">
|
||||
<div className="icon" onClick={(event) => onClickDeleteBtn(event, itemData)}>
|
||||
<IconFont type="icon-shanchu1" />
|
||||
</div>
|
||||
</div>
|
||||
) : (
|
||||
<></>
|
||||
)}
|
||||
</div>
|
||||
</List.Item>
|
||||
);
|
||||
};
|
||||
|
||||
const onFinish = () => {
|
||||
form.validateFields().then((formData) => {
|
||||
Utils.delete(api.phyCluster, {
|
||||
params: {
|
||||
clusterPhyId: clusterInfo.id,
|
||||
},
|
||||
}).then((res) => {
|
||||
message.success('删除成功');
|
||||
setVisible(false);
|
||||
props?.getPhyClusterState();
|
||||
const fliterList: any = list.filter((item: any) => {
|
||||
return item?.id !== clusterInfo.id;
|
||||
});
|
||||
setList(fliterList || []);
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
const onClickDeleteBtn = (event: any, clusterInfo: any) => {
|
||||
event.stopPropagation();
|
||||
setClusterInfo(clusterInfo);
|
||||
setVisible(true);
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
{useMemo(
|
||||
() => (
|
||||
<InfiniteScroll
|
||||
dataLength={list.length}
|
||||
next={loadMoreData}
|
||||
hasMore={list.length < pagination.total}
|
||||
loader={<Spin style={{ paddingLeft: '50%', paddingTop: 15 }} spinning={loading} />}
|
||||
endMessage={
|
||||
!pagination.total ? (
|
||||
''
|
||||
) : (
|
||||
<Divider className="load-completed-tip" plain>
|
||||
加载完成 共{pagination.total}条
|
||||
</Divider>
|
||||
)
|
||||
}
|
||||
scrollableTarget="scrollableDiv"
|
||||
>
|
||||
<List
|
||||
bordered={false}
|
||||
split={false}
|
||||
className="multi-cluster-list"
|
||||
itemLayout="horizontal"
|
||||
dataSource={list}
|
||||
renderItem={RenderItem}
|
||||
/>
|
||||
</InfiniteScroll>
|
||||
),
|
||||
[list, pagination, loading]
|
||||
)}
|
||||
|
||||
<Modal
|
||||
width={570}
|
||||
destroyOnClose={true}
|
||||
centered={true}
|
||||
className="custom-modal"
|
||||
wrapClassName="del-topic-modal delete-modal"
|
||||
title={intl.formatMessage({
|
||||
id: 'delete.cluster.confirm.title',
|
||||
})}
|
||||
visible={visible}
|
||||
onOk={onFinish}
|
||||
okText={intl.formatMessage({
|
||||
id: 'btn.delete',
|
||||
})}
|
||||
cancelText={intl.formatMessage({
|
||||
id: 'btn.cancel',
|
||||
})}
|
||||
onCancel={() => setVisible(false)}
|
||||
okButtonProps={{
|
||||
style: {
|
||||
width: 56,
|
||||
},
|
||||
danger: true,
|
||||
size: 'small',
|
||||
}}
|
||||
cancelButtonProps={{
|
||||
style: {
|
||||
width: 56,
|
||||
},
|
||||
size: 'small',
|
||||
}}
|
||||
>
|
||||
<div className="tip-info">
|
||||
<IconFont type="icon-warning-circle"></IconFont>
|
||||
<span>
|
||||
{intl.formatMessage({
|
||||
id: 'delete.cluster.confirm.tip',
|
||||
})}
|
||||
</span>
|
||||
</div>
|
||||
<Form form={form} className="form" labelCol={{ span: 4 }} wrapperCol={{ span: 16 }} autoComplete="off">
|
||||
<Form.Item label="集群名称" name="name" rules={[{ required: false, message: '' }]}>
|
||||
<span>{clusterInfo.name}</span>
|
||||
</Form.Item>
|
||||
<Form.Item
|
||||
label="集群名称"
|
||||
name="clusterName"
|
||||
rules={[
|
||||
{
|
||||
required: true,
|
||||
message: intl.formatMessage({
|
||||
id: 'delete.cluster.confirm.cluster',
|
||||
}),
|
||||
validator: (rule: any, value: string) => {
|
||||
value = value || '';
|
||||
if (!value.trim() || value.trim() !== clusterInfo.name)
|
||||
return Promise.reject(
|
||||
intl.formatMessage({
|
||||
id: 'delete.cluster.confirm.cluster',
|
||||
})
|
||||
);
|
||||
return Promise.resolve();
|
||||
},
|
||||
},
|
||||
]}
|
||||
>
|
||||
<Input />
|
||||
</Form.Item>
|
||||
</Form>
|
||||
</Modal>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default ListScroll;
|
||||
@@ -0,0 +1,155 @@
|
||||
import { FormItemType, IFormItem } from 'knowdesign/lib/extend/x-form';
|
||||
|
||||
export const bootstrapServersErrCodes = [10, 11, 12];
|
||||
export const zkErrCodes = [20, 21];
|
||||
export const jmxErrCodes = [30, 31];
|
||||
|
||||
export const statusFilters = [
|
||||
{
|
||||
label: 'Live',
|
||||
value: 1,
|
||||
},
|
||||
{
|
||||
label: 'Down',
|
||||
value: 0,
|
||||
},
|
||||
];
|
||||
|
||||
export const sortFieldList = [
|
||||
{
|
||||
label: '接入时间',
|
||||
value: 'createTime',
|
||||
},
|
||||
{
|
||||
label: '健康分',
|
||||
value: 'HealthScore',
|
||||
},
|
||||
{
|
||||
label: 'Messages',
|
||||
value: 'LeaderMessages',
|
||||
},
|
||||
{
|
||||
label: 'MessageSize',
|
||||
value: 'TotalLogSize',
|
||||
},
|
||||
{
|
||||
label: 'BytesIn',
|
||||
value: 'BytesIn',
|
||||
},
|
||||
{
|
||||
label: 'BytesOut',
|
||||
value: 'BytesOut',
|
||||
},
|
||||
{
|
||||
label: 'Brokers',
|
||||
value: 'Brokers',
|
||||
},
|
||||
];
|
||||
|
||||
export const sortTypes = [
|
||||
{
|
||||
label: '升序',
|
||||
value: 'asc',
|
||||
},
|
||||
{
|
||||
label: '降序',
|
||||
value: 'desc',
|
||||
},
|
||||
];
|
||||
|
||||
export const linesMetric = ['LeaderMessages', 'TotalLogSize', 'BytesIn', 'BytesOut'];
|
||||
export const pointsMetric = ['HealthScore', 'HealthCheckPassed', 'HealthCheckTotal', 'Brokers', 'Zookeepers', ...linesMetric].concat(
|
||||
process.env.BUSINESS_VERSION
|
||||
? ['LoadReBalanceCpu', 'LoadReBalanceDisk', 'LoadReBalanceEnable', 'LoadReBalanceNwIn', 'LoadReBalanceNwOut']
|
||||
: []
|
||||
);
|
||||
|
||||
export const metricNameMap = {
|
||||
LeaderMessages: 'Messages',
|
||||
TotalLogSize: 'LogSize',
|
||||
} as {
|
||||
[key: string]: string;
|
||||
};
|
||||
|
||||
export const healthSorceList = {
|
||||
0: 0,
|
||||
10: '',
|
||||
20: 20,
|
||||
30: '',
|
||||
40: 40,
|
||||
50: '',
|
||||
60: 60,
|
||||
70: '',
|
||||
80: 80,
|
||||
90: '',
|
||||
100: 100,
|
||||
};
|
||||
|
||||
export interface IMetricPoint {
|
||||
aggType: string;
|
||||
createTime: number;
|
||||
metricName: string;
|
||||
timeStamp: number;
|
||||
unit: string;
|
||||
updateTime: number;
|
||||
value: number;
|
||||
metricLines?: {
|
||||
name: string;
|
||||
data: [number | string, number | string];
|
||||
};
|
||||
}
|
||||
|
||||
export const getFormConfig = () => {
|
||||
return [
|
||||
{
|
||||
key: 'name',
|
||||
label: '集群名称',
|
||||
type: FormItemType.input,
|
||||
rules: [{ required: true, message: '请输入集群名称' }],
|
||||
},
|
||||
{
|
||||
key: 'bootstrap',
|
||||
label: 'Bootstrap Servers',
|
||||
type: FormItemType.textArea,
|
||||
rules: [
|
||||
{
|
||||
required: true,
|
||||
message: '请输入Bootstrap Servers',
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
key: 'Zookeeper',
|
||||
label: 'Zookeeper',
|
||||
type: FormItemType.textArea,
|
||||
rules: [
|
||||
{
|
||||
required: true,
|
||||
message: '请输入Zookeeper',
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
key: 'config',
|
||||
label: '集群配置',
|
||||
type: FormItemType.textArea,
|
||||
rules: [
|
||||
{
|
||||
required: true,
|
||||
message: '请输入集群配置',
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
key: 'desc',
|
||||
label: '集群描述',
|
||||
type: FormItemType.textArea,
|
||||
rules: [
|
||||
{
|
||||
required: true,
|
||||
message: '请输入集群描述',
|
||||
},
|
||||
],
|
||||
},
|
||||
] as unknown as IFormItem[];
|
||||
};
|
||||
@@ -0,0 +1,750 @@
|
||||
@error-color: #f46a6a;
|
||||
|
||||
.multi-cluster-page {
|
||||
position: absolute;
|
||||
left: 0;
|
||||
top: 48px;
|
||||
width: 100%;
|
||||
min-width: 1440px;
|
||||
height: calc(100% - 48px);
|
||||
overflow: auto;
|
||||
.dcloud-checkbox-wrapper {
|
||||
font-size: 13px;
|
||||
}
|
||||
&-fixed {
|
||||
position: sticky;
|
||||
top: 0;
|
||||
z-index: 100;
|
||||
width: 100%;
|
||||
.content-container {
|
||||
box-sizing: content-box;
|
||||
max-width: 1400px;
|
||||
min-width: 1200px;
|
||||
margin: 0 auto;
|
||||
padding: 24px 40px 20px 40px;
|
||||
background-image: linear-gradient(#ebebf3, #ebebf3 95%, transparent);
|
||||
}
|
||||
.multi-cluster-header {
|
||||
display: flex;
|
||||
width: 100%;
|
||||
margin-bottom: 20px;
|
||||
.dcloud-checkbox-group {
|
||||
font-size: 13px;
|
||||
}
|
||||
.dcloud-slider:hover {
|
||||
opacity: 1;
|
||||
.dcloud-slider-rail {
|
||||
background-color: #ececf1;
|
||||
}
|
||||
.dcloud-slider-track {
|
||||
background-color: #556ee6;
|
||||
background: #556ee6;
|
||||
}
|
||||
.dcloud-slider-track-1 {
|
||||
background-color: #556ee6;
|
||||
background: #556ee6;
|
||||
}
|
||||
.dcloud-slider-handle:not(.dcloud-tooltip-open) {
|
||||
border-color: #556ee6;
|
||||
}
|
||||
.dcloud-slider-handle:focus {
|
||||
border-color: #556ee6;
|
||||
background-color: #556ee6;
|
||||
background: #556ee6;
|
||||
}
|
||||
}
|
||||
.cluster-header-card {
|
||||
position: relative;
|
||||
width: 26%;
|
||||
min-width: 330px;
|
||||
max-width: 350px;
|
||||
height: 168px;
|
||||
margin-right: 12px;
|
||||
padding: 23px 30px 0;
|
||||
box-sizing: border-box;
|
||||
border-radius: 10px;
|
||||
color: #fff;
|
||||
background-image: linear-gradient(to bottom right, #556ee6, #7389f3);
|
||||
|
||||
&-bg-left {
|
||||
position: absolute;
|
||||
left: 0;
|
||||
top: 0;
|
||||
width: 113px;
|
||||
height: 135px;
|
||||
background-image: url('../../assets/leftTop.png');
|
||||
background-size: cover;
|
||||
background-position-x: right;
|
||||
border-radius: 12px;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
&-bg-right {
|
||||
position: absolute;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
width: 186px;
|
||||
height: 145px;
|
||||
background-size: cover;
|
||||
background-image: url('../../assets/rightBottom.png');
|
||||
}
|
||||
|
||||
.header-card-title {
|
||||
color: #fff;
|
||||
font-size: 16px;
|
||||
line-height: 20px;
|
||||
font-weight: normal;
|
||||
margin-bottom: 0;
|
||||
z-index: 1;
|
||||
position: relative;
|
||||
.chinese-text {
|
||||
font-size: 14px;
|
||||
}
|
||||
}
|
||||
|
||||
.header-card-total {
|
||||
font-family: DIDIFD-Medium;
|
||||
margin-bottom: 18px;
|
||||
font-size: 56px;
|
||||
line-height: 56px;
|
||||
z-index: 1;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.header-card-info {
|
||||
font-size: 13px;
|
||||
letter-spacing: 0;
|
||||
text-align: justify;
|
||||
line-height: 20px;
|
||||
|
||||
.card-info-item {
|
||||
display: inline-block;
|
||||
position: relative;
|
||||
font-size: 13px;
|
||||
letter-spacing: 0;
|
||||
text-align: justify;
|
||||
line-height: 20px;
|
||||
padding: 0 6px;
|
||||
> div {
|
||||
margin-bottom: 2px;
|
||||
}
|
||||
&-live {
|
||||
background: no-repeat url('../../assets/clusters-live-bg.png') bottom;
|
||||
background-size: 100% 12px;
|
||||
margin-left: -4px;
|
||||
margin-right: 16px;
|
||||
}
|
||||
&-down {
|
||||
background: no-repeat url('../../assets/clusters-down-bg.png') bottom;
|
||||
background-size: 100% 12px;
|
||||
}
|
||||
|
||||
.info-item-value {
|
||||
position: relative;
|
||||
font-family: DIDIFD-Black;
|
||||
font-size: 22px;
|
||||
letter-spacing: 0;
|
||||
text-align: justify;
|
||||
line-height: 16px;
|
||||
margin-left: 3px;
|
||||
|
||||
em {
|
||||
position: relative;
|
||||
font-style: normal;
|
||||
z-index: 999;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.cluster-header-filter {
|
||||
flex: 1;
|
||||
|
||||
.header-filter-top {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
margin-bottom: 12px;
|
||||
|
||||
&-input {
|
||||
flex: 1;
|
||||
|
||||
.icon {
|
||||
font-size: 16px;
|
||||
color: #adb5bc;
|
||||
}
|
||||
|
||||
.dcloud-input-group-wrapper {
|
||||
.dcloud-input-affix-wrapper {
|
||||
border-radius: 6px;
|
||||
}
|
||||
|
||||
.dcloud-input-group-addon {
|
||||
background: rgba(33, 37, 41, 0.04);
|
||||
|
||||
.dcloud-btn {
|
||||
border: none;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
&-divider {
|
||||
width: 1px;
|
||||
height: 24px;
|
||||
border: 1px solid #ced4da;
|
||||
margin: 0 18px;
|
||||
}
|
||||
|
||||
&-button {
|
||||
width: 108px;
|
||||
line-height: 19px;
|
||||
|
||||
.text {
|
||||
font-family: @font-family-bold;
|
||||
font-size: 14px;
|
||||
color: #ffffff;
|
||||
letter-spacing: 0;
|
||||
margin-left: 6px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.header-filter-bottom {
|
||||
display: flex;
|
||||
justify-content: space-around;
|
||||
height: 120px;
|
||||
box-sizing: border-box;
|
||||
padding: 24px 0px 0;
|
||||
background: #ffffff;
|
||||
box-shadow: 0 2px 4px 0 rgba(0, 0, 0, 0.01), 0 3px 6px 3px rgba(0, 0, 0, 0.01), 0 2px 6px 0 rgba(0, 0, 0, 0.03);
|
||||
border-radius: 12px;
|
||||
|
||||
&-item {
|
||||
&-checkbox {
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
&-slider {
|
||||
width: 298px;
|
||||
padding-right: 20px;
|
||||
}
|
||||
|
||||
&-title {
|
||||
margin-bottom: 24px;
|
||||
font-size: 14px;
|
||||
color: #495057;
|
||||
letter-spacing: 0;
|
||||
padding-left: 20px;
|
||||
font-family: @font-family-bold;
|
||||
&.title-right {
|
||||
padding-left: 0;
|
||||
}
|
||||
}
|
||||
|
||||
&-content {
|
||||
&.flex {
|
||||
display: flex;
|
||||
margin-right: 43px;
|
||||
|
||||
.dcloud-checkbox + span {
|
||||
padding-right: 8px;
|
||||
padding-left: 4px;
|
||||
width: 86px;
|
||||
}
|
||||
|
||||
.check-all {
|
||||
height: 26px;
|
||||
|
||||
.dcloud-checkbox + span {
|
||||
margin-right: 12px;
|
||||
width: 40px;
|
||||
font-size: 13px;
|
||||
color: #495057;
|
||||
letter-spacing: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.custom-check-group {
|
||||
display: inherit;
|
||||
width: 100%;
|
||||
position: relative;
|
||||
.check-all {
|
||||
padding-left: 20px;
|
||||
}
|
||||
|
||||
.more-check-group {
|
||||
position: absolute;
|
||||
top: 34px;
|
||||
float: left;
|
||||
padding-left: 88px;
|
||||
position: absolute;
|
||||
width: 42%;
|
||||
background: #ffffff;
|
||||
box-shadow: 0 2px 4px 0 rgba(0, 0, 0, 0.01), 0 3px 6px 3px rgba(0, 0, 0, 0.01), 0 2px 6px 0 rgba(0, 0, 0, 0.03);
|
||||
border-radius: 8px;
|
||||
border-radius: 8px;
|
||||
max-height: 92px;
|
||||
overflow-x: hidden;
|
||||
.dcloud-checkbox-group-item {
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.more-btn {
|
||||
height: 30px;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
.multi-cluster-filter {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
width: 100%;
|
||||
height: 36px;
|
||||
line-height: 38px;
|
||||
&-select {
|
||||
width: 26%;
|
||||
max-width: 350px;
|
||||
min-width: 330px;
|
||||
display: flex;
|
||||
}
|
||||
|
||||
&-checkbox {
|
||||
.dcloud-checkbox-group {
|
||||
font-size: 13px;
|
||||
.dcloud-checkbox + span {
|
||||
padding-left: 4px;
|
||||
}
|
||||
&-item:nth-child(1) {
|
||||
margin-right: 12px;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.multi-cluster-list {
|
||||
box-sizing: content-box;
|
||||
max-width: 1420px;
|
||||
min-width: 1220px;
|
||||
margin: 0 auto;
|
||||
padding: 0 30px;
|
||||
.dcloud-list-item {
|
||||
box-sizing: content-box;
|
||||
width: calc(100% - 20px);
|
||||
padding: 0;
|
||||
text-align: center;
|
||||
margin: 8px auto;
|
||||
border-radius: 12px;
|
||||
background: #fff;
|
||||
position: relative;
|
||||
transition: all 0.3s ease;
|
||||
box-shadow: 0 2px 4px 0 rgba(0, 0, 0, 0.01), 0 3px 6px 3px rgba(0, 0, 0, 0.01), 0 2px 6px 0 rgba(0, 0, 0, 0.03);
|
||||
&:first-child {
|
||||
margin-top: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.dcloud-list-item:hover {
|
||||
padding: 0 10px;
|
||||
box-shadow: 0 0 8px 0 rgba(101, 98, 240, 0.04), 0 6px 12px 12px rgba(101, 98, 240, 0.04), 0 6px 10px 0 rgba(101, 98, 240, 0.08);
|
||||
|
||||
.multi-cluster-list-item-btn {
|
||||
opacity: 1;
|
||||
.icon {
|
||||
color: #74788d;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.icon:hover {
|
||||
color: #495057;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.multi-cluster-list-item-btn {
|
||||
opacity: 0;
|
||||
position: absolute;
|
||||
right: 20px;
|
||||
top: 8px;
|
||||
z-index: 10;
|
||||
text-align: right;
|
||||
width: 24px;
|
||||
height: 24px;
|
||||
background: rgba(33, 37, 41, 0.04);
|
||||
border-radius: 14px;
|
||||
text-align: center;
|
||||
line-height: 24px;
|
||||
}
|
||||
|
||||
.multi-cluster-list-item {
|
||||
display: flex;
|
||||
width: 100%;
|
||||
height: 128px;
|
||||
margin: 0 auto;
|
||||
padding: 16px 32px 12px 32px;
|
||||
box-sizing: border-box;
|
||||
border-radius: 12px;
|
||||
cursor: pointer;
|
||||
text-align: center;
|
||||
transition: 0.5s all;
|
||||
|
||||
&-healthy {
|
||||
margin-right: 24px;
|
||||
.dcloud-progress-inner {
|
||||
border-radius: 50%;
|
||||
}
|
||||
.green-circle {
|
||||
.dcloud-progress-inner {
|
||||
background: #f5fdfc;
|
||||
}
|
||||
}
|
||||
.red-circle {
|
||||
.dcloud-progress-inner {
|
||||
background: #fffafa;
|
||||
}
|
||||
}
|
||||
.healthy-percent {
|
||||
font-family: DIDIFD-Regular;
|
||||
font-size: 40px;
|
||||
text-align: center;
|
||||
line-height: 36px;
|
||||
color: #00c0a2;
|
||||
|
||||
&.less-90 {
|
||||
color: @primary-color;
|
||||
}
|
||||
|
||||
&.no-info {
|
||||
color: #e9e7e7;
|
||||
}
|
||||
|
||||
&.down {
|
||||
font-family: @font-family-bold;
|
||||
font-size: 22px;
|
||||
color: #ff7066;
|
||||
text-align: center;
|
||||
line-height: 30px;
|
||||
}
|
||||
}
|
||||
|
||||
.healthy-degree {
|
||||
font-family: @font-family-bold;
|
||||
font-size: 12px;
|
||||
color: #495057;
|
||||
line-height: 15px;
|
||||
margin-top: 6px;
|
||||
text-align: center;
|
||||
|
||||
&-status {
|
||||
margin-right: 6px;
|
||||
color: #74788d;
|
||||
}
|
||||
|
||||
&-proportion {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
&-right {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
|
||||
.multi-cluster-list-item-base {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
width: 100%;
|
||||
padding-top: 16px;
|
||||
|
||||
&-left {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
height: 22px;
|
||||
|
||||
.base-name {
|
||||
margin-right: 8px;
|
||||
max-width: 180px;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
font-family: @font-family-bold;
|
||||
font-size: 18px;
|
||||
color: #495057;
|
||||
line-height: 22px;
|
||||
}
|
||||
|
||||
.base-version {
|
||||
height: 18px;
|
||||
font-size: 12px;
|
||||
line-height: 16px;
|
||||
background: #ececf6;
|
||||
border-radius: 4px;
|
||||
padding: 1px 6px;
|
||||
margin-right: 4px;
|
||||
}
|
||||
|
||||
.balance-box {
|
||||
height: 18px;
|
||||
margin-right: 4px;
|
||||
padding: 1px 6px;
|
||||
border-radius: 4px;
|
||||
font-size: 12px;
|
||||
line-height: 16px;
|
||||
&.balanced {
|
||||
background: rgba(85, 110, 230, 0.1);
|
||||
color: #556ee6;
|
||||
}
|
||||
&.unbalanced {
|
||||
background: rgba(255, 136, 0, 0.1);
|
||||
color: #f58342;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
&-date {
|
||||
font-size: 12px;
|
||||
color: #adb5bc;
|
||||
letter-spacing: 0;
|
||||
text-align: right;
|
||||
line-height: 18px;
|
||||
}
|
||||
}
|
||||
|
||||
.multi-cluster-list-item-Indicator {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
padding-top: 12px;
|
||||
justify-content: space-between;
|
||||
|
||||
.indicator-left {
|
||||
display: flex;
|
||||
|
||||
&-item {
|
||||
margin-right: 32px;
|
||||
|
||||
&-title {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
font-size: 14px;
|
||||
color: #74788d;
|
||||
letter-spacing: 0;
|
||||
text-align: justify;
|
||||
line-height: 16px;
|
||||
margin-bottom: 4px;
|
||||
|
||||
&-dot {
|
||||
width: 6px;
|
||||
height: 6px;
|
||||
margin-right: 3px;
|
||||
border-radius: 50%;
|
||||
}
|
||||
}
|
||||
|
||||
&-value {
|
||||
margin-left: 10px;
|
||||
font-size: 16px;
|
||||
color: #495057;
|
||||
letter-spacing: 0;
|
||||
text-align: justify;
|
||||
line-height: 20px;
|
||||
font-family: @font-family-bold;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.indicator-right {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
|
||||
&-item {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
margin-right: 40px;
|
||||
|
||||
&-total {
|
||||
margin-right: 10px;
|
||||
text-align: left;
|
||||
width: 96px;
|
||||
|
||||
&-name {
|
||||
font-size: 12px;
|
||||
color: #74788d;
|
||||
line-height: 16px;
|
||||
margin-bottom: 6px;
|
||||
}
|
||||
|
||||
&-value {
|
||||
font-size: 14px;
|
||||
color: #212529;
|
||||
letter-spacing: 0;
|
||||
line-height: 20px;
|
||||
|
||||
.total-value-unit {
|
||||
margin-left: 4px;
|
||||
font-size: 12px;
|
||||
color: #74788d;
|
||||
letter-spacing: 0;
|
||||
line-height: 14px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
&.first-right-item {
|
||||
.indicator-right-item-total {
|
||||
width: 116px;
|
||||
}
|
||||
}
|
||||
|
||||
&-chart {
|
||||
}
|
||||
}
|
||||
|
||||
&-item:last-child {
|
||||
margin-right: 0px;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.multi-cluster-list-load {
|
||||
font-size: 13px;
|
||||
color: #74788d;
|
||||
text-align: center;
|
||||
margin-top: 12px;
|
||||
height: 32px;
|
||||
line-height: 32px;
|
||||
}
|
||||
}
|
||||
.dcloud-divider-horizontal.dcloud-divider-with-text.load-completed-tip {
|
||||
box-sizing: content-box;
|
||||
width: calc(100% - 80px);
|
||||
max-width: 1400px;
|
||||
min-width: 1200px;
|
||||
padding: 16px 40px 40px 40px;
|
||||
margin: 0 auto;
|
||||
}
|
||||
}
|
||||
|
||||
.delete-modal {
|
||||
.form {
|
||||
padding-top: 16px;
|
||||
.dcloud-col-4 {
|
||||
max-width: 13.67%;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.drawer-content {
|
||||
.dcloud-form-item-extra {
|
||||
min-height: unset;
|
||||
.error-extra-info {
|
||||
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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
&.max-width-66 {
|
||||
.dcloud-form-item-control {
|
||||
max-width: 66%;
|
||||
}
|
||||
}
|
||||
|
||||
.dcloud-form-item-label {
|
||||
margin-right: 12px;
|
||||
|
||||
label {
|
||||
font-family: @font-family;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.no-item-control {
|
||||
margin-bottom: 8px !important;
|
||||
.dcloud-form-item-control {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.empty-page {
|
||||
height: 100%;
|
||||
display: flex;
|
||||
margin-top: 100px;
|
||||
align-items: center;
|
||||
flex-direction: column;
|
||||
|
||||
.header-filter-top-button {
|
||||
width: 156px;
|
||||
height: 36px;
|
||||
background: #556ee6;
|
||||
border-radius: 8px;
|
||||
|
||||
.text {
|
||||
font-family: @font-family-bold;
|
||||
font-size: 14px;
|
||||
color: #ffffff;
|
||||
letter-spacing: 0;
|
||||
padding-left: 6px;
|
||||
}
|
||||
}
|
||||
|
||||
.title {
|
||||
font-family: @font-family-bold;
|
||||
font-size: 32px;
|
||||
color: #212529;
|
||||
text-align: center;
|
||||
line-height: 48px;
|
||||
}
|
||||
|
||||
.img {
|
||||
display: flex;
|
||||
margin-top: 22px;
|
||||
margin-bottom: 39px;
|
||||
|
||||
.img-card-1 {
|
||||
width: 282px;
|
||||
height: 179px;
|
||||
background-size: cover;
|
||||
background-image: url('../../assets/dashborad.png');
|
||||
}
|
||||
|
||||
.img-card-2 {
|
||||
width: 286px;
|
||||
height: 179px;
|
||||
margin-left: 76px;
|
||||
background-size: cover;
|
||||
background-image: url('../../assets/state.png');
|
||||
}
|
||||
|
||||
.img-card-3 {
|
||||
width: 286px;
|
||||
height: 179px;
|
||||
margin-left: 76px;
|
||||
background-size: cover;
|
||||
background-image: url('../../assets/chart.png');
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user