初始化3.0.0版本

This commit is contained in:
zengqiao
2022-08-18 17:04:05 +08:00
parent 462303fca0
commit 51832385b1
2446 changed files with 93177 additions and 127211 deletions

View File

@@ -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('集群名称长度限制在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;

View File

@@ -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;

View File

@@ -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;

View File

@@ -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;

View File

@@ -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[];
};

View File

@@ -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');
}
}
}