feat: 健康状态展示优化

This commit is contained in:
GraceWalk
2022-10-28 17:33:41 +08:00
committed by EricZeng
parent 7d045dbf05
commit 5f6df3681c
30 changed files with 757 additions and 650 deletions

View File

@@ -24,6 +24,7 @@ const api = {
logout: `${securityPrefix}/account/logout`, logout: `${securityPrefix}/account/logout`,
// 全局信息 // 全局信息
getVersionInfo: () => getApi('/self/version'),
getUserInfo: (userId: number) => `${securityPrefix}/user/${userId}`, getUserInfo: (userId: number) => `${securityPrefix}/user/${userId}`,
getPermissionTree: `${securityPrefix}/permission/tree`, getPermissionTree: `${securityPrefix}/permission/tree`,
getKafkaVersionItems: () => getApi('/kafka-versions-items'), getKafkaVersionItems: () => getApi('/kafka-versions-items'),
@@ -60,6 +61,7 @@ const api = {
phyClustersDashbord: getApi(`/physical-clusters/dashboard`), phyClustersDashbord: getApi(`/physical-clusters/dashboard`),
supportKafkaVersion: getApi(`/support-kafka-versions`), supportKafkaVersion: getApi(`/support-kafka-versions`),
phyClusterState: getApi(`/physical-clusters/state`), phyClusterState: getApi(`/physical-clusters/state`),
phyClusterHealthState: getApi(`/physical-clusters/health-state`),
getOperatingStateList: (clusterPhyId: number) => getApi(`/clusters/${clusterPhyId}/groups-overview`), getOperatingStateList: (clusterPhyId: number) => getApi(`/clusters/${clusterPhyId}/groups-overview`),
getGroupTopicList: (clusterPhyId: number, groupName: string) => getApi(`/clusters/${clusterPhyId}/groups/${groupName}/topics-overview`), getGroupTopicList: (clusterPhyId: number, groupName: string) => getApi(`/clusters/${clusterPhyId}/groups/${groupName}/topics-overview`),
@@ -201,6 +203,14 @@ const api = {
getJobsTaskData: (clusterPhyId: string, jobId: string | number) => getApi(`/clusters/${clusterPhyId}/jobs/${jobId}/modify-detail`), getJobsTaskData: (clusterPhyId: string, jobId: string | number) => getApi(`/clusters/${clusterPhyId}/jobs/${jobId}/modify-detail`),
//编辑任务 //编辑任务
putJobsTaskData: (clusterPhyId: string) => getApi(`/clusters/${clusterPhyId}/jobs`), putJobsTaskData: (clusterPhyId: string) => getApi(`/clusters/${clusterPhyId}/jobs`),
// Zookeeper 接口
getZookeeperState: (clusterPhyId: string) => getApi(`/clusters/${clusterPhyId}/zookeepers-state`),
getZookeeperList: (clusterPhyId: number) => getApi(`/clusters/${clusterPhyId}/zookeepers-overview`),
getZookeeperNodeChildren: (clusterPhyId: number) => getApi(`/clusters/${clusterPhyId}/znode-children`),
getZookeeperNodeData: (clusterPhyId: number) => getApi(`/clusters/${clusterPhyId}/znode-data`),
getZookeeperMetricsInfo: (clusterPhyId: number) => getApi(`/clusters/${clusterPhyId}/zookeeper-latest-metrics`),
getZookeeperMetrics: (clusterPhyId: string) => getApi(`/clusters/${clusterPhyId}/zookeeper-metrics`),
}; };
export default api; export default api;

Binary file not shown.

After

Width:  |  Height:  |  Size: 26 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 26 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 24 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 24 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 25 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 21 KiB

View File

@@ -1,11 +1,12 @@
/* eslint-disable react/display-name */ /* eslint-disable react/display-name */
import React, { useState, useEffect } from 'react'; import React, { useState, useEffect } from 'react';
import { useHistory, useLocation, useParams } from 'react-router-dom'; import { useLocation, useParams } from 'react-router-dom';
import CardBar from '@src/components/CardBar'; import CardBar from '@src/components/CardBar';
import { healthDataProps } from '.'; import { healthDataProps } from '.';
import { Tag, Utils } from 'knowdesign'; import { Utils } from 'knowdesign';
import Api from '@src/api'; import Api from '@src/api';
import { hashDataParse } from '@src/constants/common'; import { hashDataParse } from '@src/constants/common';
import { HealthStateEnum } from '../HealthState';
export default (props: { record: any }) => { export default (props: { record: any }) => {
const { record } = props; const { record } = props;
@@ -14,22 +15,20 @@ export default (props: { record: any }) => {
const [loading, setLoading] = useState(false); const [loading, setLoading] = useState(false);
const [cardData, setCardData] = useState([]); const [cardData, setCardData] = useState([]);
const [healthData, setHealthData] = useState<healthDataProps>({ const [healthData, setHealthData] = useState<healthDataProps>({
score: 0, state: HealthStateEnum.UNKNOWN,
passed: 0, passed: 0,
total: 0, total: 0,
alive: 0,
}); });
const healthItems = ['HealthScore_Topics', 'HealthCheckPassed_Topics', 'HealthCheckTotal_Topics', 'live'];
useEffect(() => { useEffect(() => {
setLoading(true); setLoading(true);
Utils.post(Api.getBrokerDetailMetricPoints(hashDataParse(urlLocation.hash)?.brokerId, urlParams?.clusterId), [ Utils.post(Api.getBrokerDetailMetricPoints(hashDataParse(urlLocation.hash)?.brokerId, urlParams?.clusterId), [
'Partitions', 'Partitions',
'Leaders', 'Leaders',
'PartitionURP', 'PartitionURP',
'HealthScore',
'HealthCheckPassed', 'HealthCheckPassed',
'HealthCheckTotal', 'HealthCheckTotal',
'Alive', 'HealthState',
]).then((data: any) => { ]).then((data: any) => {
setLoading(false); setLoading(false);
const rightData = JSON.parse(JSON.stringify(data.metrics)); const rightData = JSON.parse(JSON.stringify(data.metrics));
@@ -47,14 +46,12 @@ export default (props: { record: any }) => {
value: rightData['PartitionURP'] || '-', value: rightData['PartitionURP'] || '-',
}, },
]; ];
const healthResData: any = {};
healthResData.score = data?.metrics?.['HealthScore'] || 0;
healthResData.passed = data?.metrics?.['HealthCheckPassed'] || 0;
healthResData.total = data?.metrics?.['HealthCheckTotal'] || 0;
healthResData.alive = data?.metrics?.['Alive'] || 0;
setCardData(cordRightMap); setCardData(cordRightMap);
setHealthData(healthResData); setHealthData({
// setCardData(data.metrics) state: data?.metrics?.['HealthState'],
passed: data?.metrics?.['HealthCheckPassed'] || 0,
total: data?.metrics?.['HealthCheckTotal'] || 0,
});
}); });
}, []); }, []);
return ( return (

View File

@@ -6,6 +6,7 @@ import { healthDataProps } from '.';
import { Tag, Tooltip, Utils } from 'knowdesign'; import { Tag, Tooltip, Utils } from 'knowdesign';
import api from '@src/api'; import api from '@src/api';
import { QuestionCircleOutlined } from '@ant-design/icons'; import { QuestionCircleOutlined } from '@ant-design/icons';
import { HealthStateEnum } from '../HealthState';
export default () => { export default () => {
const routeParams = useParams<{ const routeParams = useParams<{
@@ -14,26 +15,21 @@ export default () => {
const [loading, setLoading] = useState(false); const [loading, setLoading] = useState(false);
const [cardData, setCardData] = useState([]); const [cardData, setCardData] = useState([]);
const [healthData, setHealthData] = useState<healthDataProps>({ const [healthData, setHealthData] = useState<healthDataProps>({
score: 0, state: HealthStateEnum.UNKNOWN,
passed: 0, passed: 0,
total: 0, total: 0,
alive: 0,
}); });
const cardItems = ['Partitions', 'PartitionsSkew', 'Leaders', 'LeadersSkew', 'LogSize']; const healthItems = ['HealthCheckPassed_Brokers', 'HealthCheckTotal_Brokers', 'HealthState'];
const healthItems = ['HealthScore_Brokers', 'HealthCheckPassed_Brokers', 'HealthCheckTotal_Brokers', 'Alive'];
useEffect(() => { useEffect(() => {
setLoading(true); setLoading(true);
// 获取左侧健康度 // 获取左侧健康度
const brokerMetric = Utils.post(api.getBrokerMetricPoints(Number(routeParams.clusterId)), healthItems).then((data: any) => { const brokerMetric = Utils.post(api.getBrokerMetricPoints(Number(routeParams.clusterId)), healthItems).then((data: any) => {
const healthResData: any = {}; setHealthData({
// healthResData.score = data?.find((item:any) => item.metricName === 'HealthScore_Brokers')?.value || 0; state: data?.metrics?.['HealthState'],
// healthResData.passed = data?.find((item:any) => item.metricName === 'HealthCheckPassed_Brokers')?.value || 0; passed: data?.metrics?.['HealthCheckPassed_Brokers'] || 0,
// healthResData.total = data?.find((item:any) => item.metricName === 'HealthCheckTotal_Brokers')?.value || 0; total: data?.metrics?.['HealthCheckTotal_Brokers'] || 0,
healthResData.score = data?.metrics?.['HealthScore_Brokers'] || 0; });
healthResData.passed = data?.metrics?.['HealthCheckPassed_Brokers'] || 0;
healthResData.total = data?.metrics?.['HealthCheckTotal_Brokers'] || 0;
healthResData.alive = data?.metrics?.['Alive'] || 0;
setHealthData(healthResData);
}); });
// 获取右侧状态 // 获取右侧状态
const brokersState = Utils.request(api.getBrokersState(routeParams?.clusterId)).then((data) => { const brokersState = Utils.request(api.getBrokersState(routeParams?.clusterId)).then((data) => {
@@ -115,6 +111,6 @@ export default () => {
setLoading(false); setLoading(false);
}); });
}, [routeParams.clusterId]); }, [routeParams.clusterId]);
// console.log('cardData', cardData, healthData);
return <CardBar scene="broker" healthData={healthData} cardColumns={cardData} loading={loading}></CardBar>; return <CardBar scene="broker" healthData={healthData} cardColumns={cardData} loading={loading}></CardBar>;
}; };

View File

@@ -4,6 +4,7 @@ import CardBar from '@src/components/CardBar';
import { healthDataProps } from '.'; import { healthDataProps } from '.';
import { Utils } from 'knowdesign'; import { Utils } from 'knowdesign';
import api from '@src/api'; import api from '@src/api';
import { HealthStateEnum } from '../HealthState';
export default () => { export default () => {
const routeParams = useParams<{ const routeParams = useParams<{
@@ -12,22 +13,17 @@ export default () => {
const [loading, setLoading] = useState(false); const [loading, setLoading] = useState(false);
const [cardData, setCardData] = useState([]); const [cardData, setCardData] = useState([]);
const [healthData, setHealthData] = useState<healthDataProps>({ const [healthData, setHealthData] = useState<healthDataProps>({
score: 0, state: HealthStateEnum.UNKNOWN,
passed: 0, passed: 0,
total: 0, total: 0,
alive: 0,
}); });
const [healthDetail, setHealthDetail] = useState([]);
const cardItems = ['Groups', 'GroupActives', 'GroupEmptys', 'GroupRebalances', 'GroupDeads']; const cardItems = ['Groups', 'GroupActives', 'GroupEmptys', 'GroupRebalances', 'GroupDeads'];
const healthItems = ['HealthScore_Groups', 'HealthCheckPassed_Groups', 'HealthCheckTotal_Groups', 'Alive']; const healthItems = ['HealthCheckPassed_Groups', 'HealthCheckTotal_Groups', 'HealthState'];
useEffect(() => { useEffect(() => {
setLoading(true); setLoading(true);
Utils.post(api.getMetricPointsLatest(Number(routeParams.clusterId)), cardItems.concat(healthItems)).then((data: any) => { Utils.post(api.getMetricPointsLatest(Number(routeParams.clusterId)), cardItems.concat(healthItems)).then((data: any) => {
setLoading(false); setLoading(false);
// setCardData(data
// .filter((item: any) => cardItems.indexOf(item.metricName) >= 0)
// .map((item: any) => ({ title: item.metricName, value: item.value }))
// )
setCardData( setCardData(
cardItems.map((item) => { cardItems.map((item) => {
if (item === 'GroupDeads') { if (item === 'GroupDeads') {
@@ -36,12 +32,11 @@ export default () => {
return { title: item, value: data.metrics[item] }; return { title: item, value: data.metrics[item] };
}) })
); );
const healthResData: any = {}; setHealthData({
healthResData.score = data.metrics['HealthScore_Groups'] || 0; state: data?.metrics?.['HealthState'],
healthResData.passed = data.metrics['HealthCheckPassed_Groups'] || 0; passed: data?.metrics?.['HealthCheckPassed_Groups'] || 0,
healthResData.total = data.metrics['HealthCheckTotal_Groups'] || 0; total: data?.metrics?.['HealthCheckTotal_Groups'] || 0,
healthResData.alive = data.metrics['Alive'] || 0; });
setHealthData(healthResData);
}); });
}, []); }, []);
return <CardBar scene="group" healthData={healthData} cardColumns={cardData} loading={loading}></CardBar>; return <CardBar scene="group" healthData={healthData} cardColumns={cardData} loading={loading}></CardBar>;

View File

@@ -2,8 +2,7 @@
import React, { useState, useEffect } from 'react'; import React, { useState, useEffect } from 'react';
import { useParams } from 'react-router-dom'; import { useParams } from 'react-router-dom';
import CardBar from '@src/components/CardBar'; import CardBar from '@src/components/CardBar';
import { healthDataProps } from '.'; import { Utils } from 'knowdesign';
import { Tag, Utils } from 'knowdesign';
import Api from '@src/api'; import Api from '@src/api';
export default () => { export default () => {
@@ -12,14 +11,7 @@ export default () => {
}>(); }>();
const [loading, setLoading] = useState(false); const [loading, setLoading] = useState(false);
const [cardData, setCardData] = useState([]); const [cardData, setCardData] = useState([]);
const [healthData, setHealthData] = useState<healthDataProps>({
score: 0,
passed: 0,
total: 0,
alive: 0,
});
const cardItems = ['Partitions', 'PartitionsSkew', 'Leaders', 'LeadersSkew', 'LogSize'];
const healthItems = ['HealthScore_Brokers', 'HealthCheckPassed_Brokers', 'HealthCheckTotal_Brokers', 'alive'];
const getCordRightMap = (data: any) => { const getCordRightMap = (data: any) => {
const cordRightMap = [ const cordRightMap = [
{ {
@@ -49,6 +41,7 @@ export default () => {
]; ];
return cordRightMap; return cordRightMap;
}; };
useEffect(() => { useEffect(() => {
setLoading(true); setLoading(true);
// 获取状态 // 获取状态

View File

@@ -32,7 +32,6 @@ const LoadRebalanceCardBar = (props: any) => {
const [loading, setLoading] = useState(false); const [loading, setLoading] = useState(false);
const [cardData, setCardData] = useState([]); const [cardData, setCardData] = useState([]);
const [normsVisible, setNormsVisible] = useState(null); const [normsVisible, setNormsVisible] = useState(null);
const cardItems = ['AclEnable', 'Acls', 'AclUsers', 'AclTopics', 'AclGroups'];
const onClose = () => { const onClose = () => {
setNormsVisible(false); setNormsVisible(false);
}; };
@@ -45,11 +44,9 @@ const LoadRebalanceCardBar = (props: any) => {
// 获取右侧状态 // 获取右侧状态
getCartInfo() getCartInfo()
.then((res: any) => { .then((res: any) => {
// const { AclEnable, Acls, AclUsers, AclTopics, AclGroups } = res.metrics;
const { next, sub, status } = res; const { next, sub, status } = res;
const { cpu, disk, bytesIn, bytesOut } = sub; const { cpu, disk, bytesIn, bytesOut } = sub;
const newNextDate: any = transUnitTimePro(moment(next).valueOf() - moment().valueOf()); const newNextDate: any = transUnitTimePro(moment(next).valueOf() - moment().valueOf());
// const newNextDate = parseInt(`${transUnitTimePro(moment(next).valueOf() - moment().valueOf())}`);
const cardMap = [ const cardMap = [
{ {
title() { title() {
@@ -80,20 +77,15 @@ const LoadRebalanceCardBar = (props: any) => {
> >
{!status ? '已均衡' : '未均衡'} {!status ? '已均衡' : '未均衡'}
</Tag> </Tag>
{/* <Tag style={{ padding: '2px 4px', backgroundColor: 'rgba(85,110,230,0.10)', color: '#556EE6' }}>已均衡</Tag> */}
</div> </div>
<div style={{ display: 'flex', justifyContent: 'space-between' }}> <div style={{ display: 'flex', justifyContent: 'space-between' }}>
<span> <span>
<IconFont className="cutomIcon" type={`${!status ? 'icon-zhengchang' : 'icon-warning'}`} /> <IconFont className="cutomIcon" type={`${!status ? 'icon-zhengchang' : 'icon-warning'}`} />
</span> </span>
{/* <span>
周期均衡 <IconFont className="cutomIcon" type="icon-zhengchang" />
</span> */}
<span> <span>
{newNextDate?.value || 0} {newNextDate?.value || 0}
{newNextDate?.unit || '分钟'} {newNextDate?.unit || '分钟'}
</span> </span>
{/* {<span>距下次均衡还剩{1}小时</span>} */}
</div> </div>
</div> </div>
); );
@@ -106,73 +98,6 @@ const LoadRebalanceCardBar = (props: any) => {
padding: '12px 12px 8px 12px', padding: '12px 12px 8px 12px',
}, },
}, },
// {
// // title: 'CPU avg',
// title() {
// return (
// <div>
// <span style={{ display: 'inline-block', marginRight: '8px' }}>CPU AVG</span>
// {!cpu?.interval && cpu?.interval !== 0 && (
// <Tooltip overlayClassName="rebalance-tooltip" title="未设置均衡策略">
// <QuestionCircleOutlined />
// </Tooltip>
// )}
// {/* <IconFont className="cutomIcon" onClick={() => setNormsVisible(true)} type="icon-shezhi"></IconFont> */}
// </div>
// );
// },
// value(visibleType: boolean) {
// return (
// <div id="CPU" style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'flex-end' }}>
// <div style={{ display: 'inline-block' }}>
// <div style={{ margin: '5px 0', fontFamily: 'DIDIFD-Medium' }}>
// <span style={{ fontSize: '24px' }}>{cpu?.avg || 0}</span>
// <span style={{ fontSize: '14px', display: 'inline-block', marginLeft: '4px' }}>%</span>
// </div>
// <div style={{ marginTop: '-4px', display: 'flex', justifyContent: 'space-between' }}>
// <span>均衡区间: ±{cpu?.interval || 0}%</span>
// </div>
// </div>
// <Popover
// // visible={visibleType} // 修改为hover柱状图
// overlayClassName="custom-popover"
// content={
// <div style={{ color: '#495057' }}>
// <div>
// <IconFont className="cutomIcon cutomIcon-red" type="icon-chaoguo" />
// 超过均衡区间的有: {cpu?.bigNu || 0}
// </div>
// <div style={{ margin: '6px 0' }}>
// <IconFont className="cutomIcon cutomIcon-green" type="icon-qujian" />
// 在均衡区间内的有: {cpu?.betweenNu || 0}
// </div>
// <div>
// <IconFont className="cutomIcon cutomIcon-red" type="icon-diyu" />
// 低于均衡区间的有: {cpu?.smallNu || 0}
// </div>
// </div>
// }
// getPopupContainer={(triggerNode: any) => {
// return triggerNode;
// }}
// color="#ffffff"
// >
// <div style={{ width: '44px', height: '30px' }}>
// <StateChart
// data={[
// { name: 'bigNu', value: cpu?.bigNu || 0 },
// { name: 'betweenNu', value: cpu?.betweenNu || 0 },
// { name: 'smallNu', value: cpu?.smallNu || 0 },
// ]}
// />
// </div>
// </Popover>
// </div>
// );
// },
// className: 'custom-card-bar',
// valueClassName: 'custom-card-bar-value',
// },
{ {
title() { title() {
return ( return (

View File

@@ -5,8 +5,10 @@ import { healthDataProps } from '.';
import { Utils } from 'knowdesign'; import { Utils } from 'knowdesign';
import { IconFont } from '@knowdesign/icons'; import { IconFont } from '@knowdesign/icons';
import api from '@src/api'; import api from '@src/api';
import { healthScoreCondition } from './const';
import { hashDataParse } from '@src/constants/common'; import { hashDataParse } from '@src/constants/common';
import { HealthStateEnum } from '../HealthState';
const healthItems = ['HealthCheckPassed', 'HealthCheckTotal', 'HealthState'];
const renderValue = (v: string | number | ((visibleType?: boolean) => JSX.Element), visibleType?: boolean) => { const renderValue = (v: string | number | ((visibleType?: boolean) => JSX.Element), visibleType?: boolean) => {
return typeof v === 'function' ? v(visibleType) : v; return typeof v === 'function' ? v(visibleType) : v;
@@ -19,14 +21,12 @@ export default (props: { record: any }) => {
const [loading, setLoading] = useState(false); const [loading, setLoading] = useState(false);
const [cardData, setCardData] = useState([]); const [cardData, setCardData] = useState([]);
const [healthData, setHealthData] = useState<healthDataProps>({ const [healthData, setHealthData] = useState<healthDataProps>({
score: 0, state: HealthStateEnum.UNKNOWN,
passed: 0, passed: 0,
total: 0, total: 0,
alive: 0,
}); });
const [healthDetail, setHealthDetail] = useState([]);
const [clusterAlive, setClusterAlive] = useState(0); const [clusterAlive, setClusterAlive] = useState(0);
const healthItems = ['HealthScore', 'HealthCheckPassed', 'HealthCheckTotal', 'alive'];
const getNumAndSubTitles = (cardColumnsItemData: any) => { const getNumAndSubTitles = (cardColumnsItemData: any) => {
return ( return (
<div style={{ width: '100%', display: 'flex', alignItems: 'end' }}> <div style={{ width: '100%', display: 'flex', alignItems: 'end' }}>
@@ -40,21 +40,21 @@ export default (props: { record: any }) => {
</div> </div>
); );
}; };
useEffect(() => { useEffect(() => {
setLoading(true); setLoading(true);
const topicName = hashDataParse(location.hash)['topicName']; const topicName = hashDataParse(location.hash)['topicName'];
let detailHealthPromise = Utils.post(api.getTopicMetricPointsLatest(Number(routeParams.clusterId), topicName), healthItems).then( const detailHealthPromise = Utils.post(api.getTopicMetricPointsLatest(Number(routeParams.clusterId), topicName), healthItems).then(
(data: any) => { (data: any) => {
let healthResData: any = {}; setHealthData({
healthResData.score = data.metrics['HealthScore'] || 0; state: data.metrics['HealthState'],
healthResData.passed = data.metrics['HealthCheckPassed'] || 0; passed: data.metrics['HealthCheckPassed'] || 0,
healthResData.total = data.metrics['HealthCheckTotal'] || 0; total: data.metrics['HealthCheckTotal'] || 0,
// healthResData.alive = data.metrics['alive'] || 0 });
setHealthData(healthResData);
} }
); );
let detailStatePromise = Utils.request(api.getTopicState(Number(routeParams.clusterId), topicName)).then((topicHealthState: any) => { const detailStatePromise = Utils.request(api.getTopicState(Number(routeParams.clusterId), topicName)).then((topicHealthState: any) => {
setCardData([ setCardData([
{ {
title: 'Partitions', title: 'Partitions',
@@ -87,13 +87,12 @@ export default (props: { record: any }) => {
]); ]);
}); });
// 获取集群维度的指标信息 // 获取集群维度的指标信息
let clusterStatePromise = Utils.post(api.getMetricPointsLatest(Number(routeParams.clusterId)), ['Alive']).then( const clusterStatePromise = Utils.post(api.getMetricPointsLatest(Number(routeParams.clusterId)), ['Alive']).then(
(clusterHealthState: any) => { (clusterHealthState: any) => {
let clusterAlive = clusterHealthState?.metrics?.Alive || 0; setClusterAlive(clusterHealthState?.metrics?.Alive || 0);
setClusterAlive(clusterAlive);
} }
); );
Promise.all([detailHealthPromise, detailStatePromise, clusterStatePromise]).then((res) => { Promise.all([detailHealthPromise, detailStatePromise, clusterStatePromise]).then(() => {
setLoading(false); setLoading(false);
}); });
}, []); }, []);
@@ -101,7 +100,7 @@ export default (props: { record: any }) => {
<CardBar <CardBar
record={record} record={record}
scene="topic" scene="topic"
healthData={{ ...healthData, alive: clusterAlive }} healthData={{ ...healthData, state: clusterAlive ? healthData.state : HealthStateEnum.DOWN }}
cardColumns={cardData} cardColumns={cardData}
showCardBg={false} showCardBg={false}
loading={loading} loading={loading}

View File

@@ -4,6 +4,7 @@ import CardBar from '@src/components/CardBar';
import { healthDataProps } from '.'; import { healthDataProps } from '.';
import { Utils } from 'knowdesign'; import { Utils } from 'knowdesign';
import api from '@src/api'; import api from '@src/api';
import { HealthStateEnum } from '../HealthState';
export default () => { export default () => {
const routeParams = useParams<{ const routeParams = useParams<{
@@ -12,14 +13,12 @@ export default () => {
const [loading, setLoading] = useState(false); const [loading, setLoading] = useState(false);
const [cardData, setCardData] = useState([]); const [cardData, setCardData] = useState([]);
const [healthData, setHealthData] = useState<healthDataProps>({ const [healthData, setHealthData] = useState<healthDataProps>({
score: 0, state: HealthStateEnum.UNKNOWN,
passed: 0, passed: 0,
total: 0, total: 0,
alive: 0,
}); });
const [healthDetail, setHealthDetail] = useState([]);
const cardItems = ['Topics', 'Partitions', 'PartitionNoLeader', 'PartitionMinISR_S', 'PartitionMinISR_E', 'PartitionURP']; const cardItems = ['Topics', 'Partitions', 'PartitionNoLeader', 'PartitionMinISR_S', 'PartitionMinISR_E', 'PartitionURP'];
const healthItems = ['HealthScore_Topics', 'HealthCheckPassed_Topics', 'HealthCheckTotal_Topics', 'Alive']; const healthItems = ['HealthCheckPassed_Topics', 'HealthCheckTotal_Topics', 'HealthState'];
useEffect(() => { useEffect(() => {
setLoading(true); setLoading(true);
Utils.post(api.getMetricPointsLatest(Number(routeParams.clusterId)), cardItems.concat(healthItems)).then((data: any) => { Utils.post(api.getMetricPointsLatest(Number(routeParams.clusterId)), cardItems.concat(healthItems)).then((data: any) => {
@@ -42,12 +41,6 @@ export default () => {
PartitionURP: 'URP', PartitionURP: 'URP',
PartitionNoLeader: 'No Leader', PartitionNoLeader: 'No Leader',
}; };
// setCardData(data
// .filter(item => cardItems.indexOf(item.name) >= 0)
// .map(item => {
// return { title: metricElmMap[item.name] || item.name, value: item.value }
// })
// )
setCardData( setCardData(
cardItems.map((item) => { cardItems.map((item) => {
let title = item; let title = item;
@@ -66,12 +59,11 @@ export default () => {
return { title, value: data.metrics[item] }; return { title, value: data.metrics[item] };
}) })
); );
const healthResData: any = {}; setHealthData({
healthResData.score = data.metrics['HealthScore_Topics'] || 0; state: data.metrics['HealthState'],
healthResData.passed = data.metrics['HealthCheckPassed_Topics'] || 0; passed: data.metrics['HealthCheckPassed_Topics'] || 0,
healthResData.total = data.metrics['HealthCheckTotal_Topics'] || 0; total: data.metrics['HealthCheckTotal_Topics'] || 0,
healthResData.alive = data.metrics['Alive'] || 0; });
setHealthData(healthResData);
}); });
}, []); }, []);
return <CardBar scene="topic" healthData={healthData} cardColumns={cardData} loading={loading}></CardBar>; return <CardBar scene="topic" healthData={healthData} cardColumns={cardData} loading={loading}></CardBar>;

View File

@@ -0,0 +1,120 @@
import React, { useState, useEffect } from 'react';
import { useParams } from 'react-router-dom';
import CardBar, { healthDataProps } from './index';
import { Utils } from 'knowdesign';
import api from '@src/api';
import { HealthStateEnum } from '../HealthState';
interface ZookeeperState {
aliveFollowerCount: number;
aliveObserverCount: number;
aliveServerCount: number;
healthCheckPassed: number;
healthCheckTotal: number;
healthState: number;
leaderNode: string;
totalFollowerCount: number;
totalObserverCount: number;
totalServerCount: number;
watchCount: number;
}
const getVal = (val: string | number | undefined | null) => {
return val === undefined || val === null || val === '' ? '-' : val;
};
const ZookeeperCard = () => {
const { clusterId } = useParams<{
clusterId: string;
}>();
const [loading, setLoading] = useState(false);
const [cardData, setCardData] = useState([]);
const [healthData, setHealthData] = useState<healthDataProps>({
state: HealthStateEnum.UNKNOWN,
passed: 0,
total: 0,
});
const getHealthData = () => {
return Utils.post(api.getZookeeperMetricsInfo(Number(clusterId)), ['HealthCheckPassed', 'HealthCheckTotal', 'HealthState']).then(
(data: any) => {
setHealthData({
state: data?.metrics?.['HealthState'],
passed: data?.metrics?.['HealthCheckPassed'] || 0,
total: data?.metrics?.['HealthCheckTotal'] || 0,
});
}
);
};
const getCardInfo = () => {
return Utils.request(api.getZookeeperState(clusterId)).then((res: ZookeeperState) => {
const {
aliveFollowerCount,
aliveObserverCount,
aliveServerCount,
totalFollowerCount,
totalObserverCount,
totalServerCount,
watchCount,
leaderNode,
} = res || {};
const cardMap = [
{
title: 'Node Count',
value() {
return (
<span>
{aliveServerCount || '-'}/{totalServerCount || '-'}
</span>
);
},
customStyle: {
// 自定义cardbar样式
marginLeft: 0,
},
},
{
title: 'Watch Count',
value: getVal(watchCount),
},
{
title: 'Leader',
value() {
return <span style={{ fontSize: 24 }}>{leaderNode || '-'}</span>;
},
},
{
title: 'Follower',
value() {
return (
<span>
{getVal(aliveFollowerCount)}/{getVal(totalFollowerCount)}
</span>
);
},
},
{
title: 'Observer',
value() {
return (
<span>
{getVal(aliveObserverCount)}/{getVal(totalObserverCount)}
</span>
);
},
},
];
setCardData(cardMap);
});
};
useEffect(() => {
setLoading(true);
Promise.all([getHealthData(), getCardInfo()]).finally(() => {
setLoading(false);
});
}, [clusterId]);
return <CardBar scene="zookeeper" healthData={healthData} cardColumns={cardData} loading={loading}></CardBar>;
};
export default ZookeeperCard;

View File

@@ -10,48 +10,15 @@
height: 88px; height: 88px;
width: 100%; width: 100%;
display: flex; display: flex;
// justify-content: space-between;
align-items: center; align-items: center;
.card-bar-health { .card-bar-health {
width: 240px; width: 240px;
height: 70px; height: 70px;
display: flex; display: flex;
align-items: center; align-items: center;
// justify-content: space-between;
.card-bar-health-process { .card-bar-health-process {
height: 100%; padding-top: 30px;
margin-right: 24px; margin-right: 20px;
.dcloud-progress-inner {
border-radius: 50%;
}
.dcloud-progress-status-normal {
.dcloud-progress-inner {
background: rgba(85, 110, 230, 0.03);
}
.dcloud-progress-inner:not(.dcloud-progress-circle-gradient) .dcloud-progress-circle-path {
stroke: rgb(85, 110, 230);
}
}
.dcloud-progress-status-success {
.dcloud-progress-inner {
background: rgba(0, 192, 162, 0.03);
}
.dcloud-progress-inner:not(.dcloud-progress-circle-gradient) .dcloud-progress-circle-path {
stroke: rgb(0, 192, 162);
}
}
.dcloud-progress-status-exception {
.dcloud-progress-inner {
background: rgba(255, 112, 102, 0.03);
}
.dcloud-progress-inner:not(.dcloud-progress-circle-gradient) .dcloud-progress-circle-path {
stroke: rgb(255, 112, 102);
}
}
.dcloud-progress-inner {
font-family: DIDIFD-Regular;
font-size: 40px !important;
}
} }
.state { .state {
font-size: 13px; font-size: 13px;
@@ -61,20 +28,6 @@
line-height: 20px; line-height: 20px;
display: flex; display: flex;
align-items: center; align-items: center;
.health-status-image {
width: 15px;
height: 15px;
background-size: cover;
}
.health-status-image-success {
background-image: url('../../assets/health-status-success.png');
}
.health-status-image-exception {
background-image: url('../../assets/health-status-exception.png');
}
.health-status-image-normal {
background-image: url('../../assets/health-status-normal.png');
}
} }
.value-bar { .value-bar {
display: flex; display: flex;

View File

@@ -1,25 +1,24 @@
import React, { useEffect, useState } from 'react'; import React, { useEffect, useState } from 'react';
import { useParams } from 'react-router-dom'; import { useParams } from 'react-router-dom';
import { Drawer, Select, Spin, Table } from 'knowdesign'; import { Drawer, Spin, Table, Utils } from 'knowdesign';
import { IconFont } from '@knowdesign/icons'; import { IconFont } from '@knowdesign/icons';
import { Utils, Progress } from 'knowdesign';
import './index.less'; import './index.less';
import api from '@src/api'; import api from '@src/api';
import moment from 'moment'; import moment from 'moment';
import TagsWithHide from '../TagsWithHide/index'; import TagsWithHide from '../TagsWithHide/index';
import { getHealthProcessColor } from '@src/pages/SingleClusterDetail/config'; import HealthState, { getHealthStateDesc, getHealthStateEmoji, HealthStateEnum } from '../HealthState';
import { getConfigItemDetailDesc } from '@src/pages/SingleClusterDetail/config';
export interface healthDataProps { export interface healthDataProps {
score: number; state: HealthStateEnum;
passed: number; passed: number;
total: number; total: number;
alive: number;
} }
export interface CardBarProps { export interface CardBarProps {
cardColumns?: any[]; cardColumns?: any[];
healthData?: healthDataProps; healthData?: healthDataProps;
showCardBg?: boolean; showCardBg?: boolean;
scene: 'topic' | 'broker' | 'group'; scene: 'topic' | 'broker' | 'group' | 'zookeeper';
record?: any; record?: any;
loading?: boolean; loading?: boolean;
needProgress?: boolean; needProgress?: boolean;
@@ -27,36 +26,27 @@ export interface CardBarProps {
const renderValue = (v: string | number | ((visibleType?: boolean) => JSX.Element), visibleType?: boolean) => { const renderValue = (v: string | number | ((visibleType?: boolean) => JSX.Element), visibleType?: boolean) => {
return typeof v === 'function' ? v(visibleType) : v; return typeof v === 'function' ? v(visibleType) : v;
}; };
const statusTxtEmojiMap = {
success: {
emoji: '👍',
txt: '优异',
},
normal: {
emoji: '😊',
txt: '正常',
},
exception: {
emoji: '👻',
txt: '异常',
},
};
const sceneCodeMap = { const sceneCodeMap = {
topic: {
code: 2,
fieldName: 'topicName',
alias: 'Topics',
},
broker: { broker: {
code: 1, code: 1,
fieldName: 'brokerId', fieldName: 'brokerId',
alias: 'Brokers', alias: 'Brokers',
}, },
topic: {
code: 2,
fieldName: 'topicName',
alias: 'Topics',
},
group: { group: {
code: 3, code: 3,
fieldName: 'groupName', fieldName: 'groupName',
alias: 'Consumers', alias: 'Consumers',
}, },
zookeeper: {
code: 4,
fieldName: 'zookeeperId',
alias: 'Zookeeper',
},
}; };
const CardColumnsItem: any = (cardItem: any) => { const CardColumnsItem: any = (cardItem: any) => {
const { cardColumnsItemData, showCardBg } = cardItem; const { cardColumnsItemData, showCardBg } = cardItem;
@@ -92,16 +82,7 @@ const CardBar = (props: CardBarProps) => {
}>(); }>();
const { healthData, cardColumns, showCardBg = true, scene, record, loading, needProgress = true } = props; const { healthData, cardColumns, showCardBg = true, scene, record, loading, needProgress = true } = props;
const [detailDrawerVisible, setDetailDrawerVisible] = useState(false); const [detailDrawerVisible, setDetailDrawerVisible] = useState(false);
const [progressStatus, setProgressStatus] = useState<'success' | 'exception' | 'normal'>('success');
const [healthCheckDetailList, setHealthCheckDetailList] = useState([]); const [healthCheckDetailList, setHealthCheckDetailList] = useState([]);
const [isAlive, setIsAlive] = useState(true);
useEffect(() => {
if (healthData) {
setProgressStatus(!isAlive ? 'exception' : healthData.score >= 90 ? 'success' : 'normal');
setIsAlive(healthData.alive === 1);
}
}, [healthData, isAlive]);
useEffect(() => { useEffect(() => {
const sceneObj = sceneCodeMap[scene]; const sceneObj = sceneCodeMap[scene];
@@ -120,23 +101,24 @@ const CardBar = (props: CardBarProps) => {
const columns = [ const columns = [
{ {
title: '检查项', title: '检查项',
dataIndex: 'configDesc', dataIndex: 'checkConfig',
key: 'configDesc', render(config: any, record: any) {
}, let valueGroup = {};
{ try {
title: '权重', valueGroup = JSON.parse(config.value);
dataIndex: 'weightPercent', } catch (e) {
key: 'weightPercent', //
}, }
{ return getConfigItemDetailDesc(record.configItem, valueGroup) || record.configDesc || '-';
title: '得分', },
dataIndex: 'score',
key: 'score',
}, },
// {
// title: '得分',
// dataIndex: 'score',
// },
{ {
title: '检查时间', title: '检查时间',
dataIndex: 'updateTime', dataIndex: 'updateTime',
key: 'updateTime',
render: (value: number) => { render: (value: number) => {
return moment(value).format('YYYY-MM-DD HH:mm:ss'); return moment(value).format('YYYY-MM-DD HH:mm:ss');
}, },
@@ -144,8 +126,6 @@ const CardBar = (props: CardBarProps) => {
{ {
title: '检查结果', title: '检查结果',
dataIndex: 'passed', dataIndex: 'passed',
key: 'passed',
width: 280,
render(value: boolean, record: any) { render(value: boolean, record: any) {
const icon = value ? <IconFont type="icon-zhengchang"></IconFont> : <IconFont type="icon-yichang"></IconFont>; const icon = value ? <IconFont type="icon-zhengchang"></IconFont> : <IconFont type="icon-yichang"></IconFont>;
const txt = value ? '已通过' : '未通过'; const txt = value ? '已通过' : '未通过';
@@ -168,40 +148,13 @@ const CardBar = (props: CardBarProps) => {
{!loading && healthData && needProgress && ( {!loading && healthData && needProgress && (
<div className="card-bar-health"> <div className="card-bar-health">
<div className="card-bar-health-process"> <div className="card-bar-health-process">
<Progress <HealthState state={healthData?.state} width={74} height={74} />
width={70}
type="circle"
percent={!isAlive ? 100 : healthData.score}
status={progressStatus}
format={(percent, successPercent) => {
return !isAlive ? (
<div
style={{
fontFamily: 'HelveticaNeue-Medium',
fontSize: 22,
color: getHealthProcessColor(healthData.score, healthData.alive),
}}
>
Down
</div>
) : (
<div
style={{
textIndent: Math.round(percent) >= 100 ? '-4px' : '',
color: getHealthProcessColor(healthData.score, healthData.alive),
}}
>
{Math.round(percent)}
</div>
);
}}
strokeWidth={3}
/>
</div> </div>
<div> <div>
<div className="state"> <div className="state">
<div className={`health-status-image health-status-image-${progressStatus}`}></div> {getHealthStateEmoji(healthData?.state)}
&nbsp;{sceneCodeMap[scene].alias}{statusTxtEmojiMap[progressStatus].txt} &nbsp;{sceneCodeMap[scene].alias}
{getHealthStateDesc(healthData?.state)}
</div> </div>
<div className="value-bar"> <div className="value-bar">
<div className="value">{`${healthData?.passed}/${healthData?.total}`}</div> <div className="value">{`${healthData?.passed}/${healthData?.total}`}</div>

View File

@@ -0,0 +1,6 @@
.health-state {
img {
width: 100%;
height: 100%;
}
}

View File

@@ -0,0 +1,76 @@
import React from 'react';
import GoodState from '@src/assets/health-good.png';
import MediumState from '@src/assets/health-medium.png';
import PoorState from '@src/assets/health-poor.png';
import DownState from '@src/assets/health-down.png';
import UnknownState from '@src/assets/health-unknown.png';
import GoodStateEmoji from '@src/assets/health-good-emoji.png';
import MediumStateEmoji from '@src/assets/health-medium-emoji.png';
import PoorStateEmoji from '@src/assets/health-poor-emoji.png';
import DownStateEmoji from '@src/assets/health-down-emoji.png';
import './index.less';
export enum HealthStateEnum {
UNKNOWN = -1,
GOOD,
MEDIUM,
POOR,
DOWN,
}
interface HealthStateProps {
state: HealthStateEnum;
width: string | number;
height: string | number;
}
const HEALTH_STATE_MAP = {
[HealthStateEnum.GOOD]: GoodState,
[HealthStateEnum.MEDIUM]: MediumState,
[HealthStateEnum.POOR]: PoorState,
[HealthStateEnum.DOWN]: DownState,
[HealthStateEnum.UNKNOWN]: UnknownState,
};
const HEALTH_STATE_EMOJI_MAP = {
[HealthStateEnum.GOOD]: GoodStateEmoji,
[HealthStateEnum.MEDIUM]: MediumStateEmoji,
[HealthStateEnum.POOR]: PoorStateEmoji,
[HealthStateEnum.DOWN]: DownStateEmoji,
[HealthStateEnum.UNKNOWN]: DownStateEmoji,
};
const HEALTH_STATE_DESC_MAP = {
[HealthStateEnum.GOOD]: '状态优异',
[HealthStateEnum.MEDIUM]: '状态良好',
[HealthStateEnum.POOR]: '状态较差',
[HealthStateEnum.DOWN]: '状态异常',
[HealthStateEnum.UNKNOWN]: '状态异常',
};
export const getHealthStateEmoji = (state: HealthStateEnum, width = 16, height = 16) => {
return (
<img
width={width}
height={height}
style={{ marginTop: -3 }}
src={HEALTH_STATE_EMOJI_MAP[state] || HEALTH_STATE_EMOJI_MAP[HealthStateEnum.UNKNOWN]}
/>
);
};
export const getHealthStateDesc = (state: HealthStateEnum) => {
return HEALTH_STATE_DESC_MAP[state] || HEALTH_STATE_DESC_MAP[HealthStateEnum.UNKNOWN];
};
const HealthState = (props: HealthStateProps) => {
const { state, width, height } = props;
return (
<div className="health-state" style={{ width, height }}>
<img src={HEALTH_STATE_MAP[state] || UnknownState} />
</div>
);
};
export default HealthState;

View File

@@ -1,14 +1,14 @@
import React, { useEffect, useRef, useState } from 'react'; import React, { useEffect, useRef, useState } from 'react';
import { Slider, Input, Select, Checkbox, Button, Utils, Spin, AppContainer } from 'knowdesign'; import { Slider, Input, Select, Checkbox, Button, Utils, Spin, AppContainer, Tooltip } from 'knowdesign';
import { IconFont } from '@knowdesign/icons'; import { IconFont } from '@knowdesign/icons';
import API from '@src/api'; import API from '@src/api';
import TourGuide, { MultiPageSteps } from '@src/components/TourGuide'; import TourGuide, { MultiPageSteps } from '@src/components/TourGuide';
import './index.less'; import { healthSorceList, sliderValueMap, sortFieldList, sortTypes, statusFilters } from './config';
import { healthSorceList, sortFieldList, sortTypes, statusFilters } from './config';
import ClusterList from './List'; import ClusterList from './List';
import AccessClusters from './AccessCluster'; import AccessClusters from './AccessCluster';
import CustomCheckGroup from './CustomCheckGroup'; import CustomCheckGroup from './CustomCheckGroup';
import { ClustersPermissionMap } from '../CommonConfig'; import { ClustersPermissionMap } from '../CommonConfig';
import './index.less';
const CheckboxGroup = Checkbox.Group; const CheckboxGroup = Checkbox.Group;
const { Option } = Select; const { Option } = Select;
@@ -19,8 +19,17 @@ interface ClustersState {
total: number; total: number;
} }
interface ClustersHealthState {
deadCount: number;
goodCount: number;
mediumCount: number;
poorCount: number;
total: number;
unknownCount: number;
}
export interface SearchParams { export interface SearchParams {
healthScoreRange?: [number, number]; healthState?: number[];
checkedKafkaVersions?: string[]; checkedKafkaVersions?: string[];
sortInfo?: { sortInfo?: {
sortField: string; sortField: string;
@@ -74,16 +83,24 @@ const MultiClusterPage = () => {
liveCount: 0, liveCount: 0,
total: 0, total: 0,
}); });
const [clustersHealthState, setClustersHealthState] = React.useState<ClustersHealthState>();
const [sliderInfo, setSliderInfo] = React.useState<{
value: [number, number];
desc: string;
}>({
value: [0, 5],
desc: '',
});
// TODO: 首次进入因 searchParams 状态变化导致获取两次列表数据的问题 // TODO: 首次进入因 searchParams 状态变化导致获取两次列表数据的问题
const [searchParams, setSearchParams] = React.useState<SearchParams>({ const [searchParams, setSearchParams] = React.useState<SearchParams>({
keywords: '', keywords: '',
checkedKafkaVersions: [], checkedKafkaVersions: [],
healthScoreRange: [0, 100],
sortInfo: { sortInfo: {
sortField: 'HealthScore', sortField: 'HealthState',
sortType: 'asc', sortType: 'asc',
}, },
clusterStatus: [0, 1], clusterStatus: [0, 1],
healthState: [-1, 0, 1, 2, 3],
// 是否拉取当前所有数据 // 是否拉取当前所有数据
isReloadAll: false, isReloadAll: false,
}); });
@@ -91,10 +108,28 @@ const MultiClusterPage = () => {
const searchKeyword = useRef(''); const searchKeyword = useRef('');
const isReload = useRef(false); const isReload = useRef(false);
const getPhyClusterHealthState = () => {
Utils.request(API.phyClusterHealthState).then((res: ClustersHealthState) => {
setClustersHealthState(res || undefined);
const result: string[] = [];
for (let i = 0; i < sliderInfo.value[1] - sliderInfo.value[0]; i++) {
const val = sliderValueMap[(sliderInfo.value[1] - i) as keyof typeof sliderValueMap];
result.push(`${val.name}: ${res?.[val.key as keyof ClustersHealthState]}`);
}
setSliderInfo((cur) => ({
...cur,
desc: result.reverse().join(', '),
}));
});
};
// 获取集群状态 // 获取集群状态
const getPhyClusterState = () => { const getPhyClusterState = () => {
getPhyClusterHealthState();
Utils.request(API.phyClusterState) Utils.request(API.phyClusterState)
.then((res: any) => { .then((res: ClustersState) => {
setStateInfo(res); setStateInfo(res);
}) })
.finally(() => { .finally(() => {
@@ -111,13 +146,14 @@ const MultiClusterPage = () => {
const updateSearchParams = (params: SearchParams) => { const updateSearchParams = (params: SearchParams) => {
setSearchParams((curParams) => ({ ...curParams, isReloadAll: false, ...params })); setSearchParams((curParams) => ({ ...curParams, isReloadAll: false, ...params }));
getPhyClusterHealthState();
}; };
const searchParamsChangeFunc = { const searchParamsChangeFunc = {
// 健康分改变 // 健康分改变
onSilderChange: (value: [number, number]) => onSilderChange: (value: number[]) =>
updateSearchParams({ updateSearchParams({
healthScoreRange: value, healthState: value,
}), }),
// 排序信息改变 // 排序信息改变
onSortInfoChange: (type: string, value: string) => onSortInfoChange: (type: string, value: string) =>
@@ -251,16 +287,41 @@ const MultiClusterPage = () => {
</div> </div>
</div> </div>
<div className="header-filter-bottom-item header-filter-bottom-item-slider"> <div className="header-filter-bottom-item header-filter-bottom-item-slider">
<h3 className="header-filter-bottom-item-title title-right"></h3> <h3 className="header-filter-bottom-item-title title-right"></h3>
<div className="header-filter-bottom-item-content"> <Tooltip title={sliderInfo.desc} overlayClassName="cluster-health-state-tooltip">
<Slider <div className="header-filter-bottom-item-content" id="clusters-slider">
range <Slider
step={20} dots
defaultValue={[0, 100]} range={{ draggableTrack: true }}
marks={healthSorceList} step={1}
onAfterChange={searchParamsChangeFunc.onSilderChange} max={5}
/> marks={healthSorceList}
</div> value={sliderInfo.value}
tooltipVisible={false}
onChange={(value: [number, number]) => {
if (value[0] !== value[1]) {
const result = [];
for (let i = 0; i < value[1] - value[0]; i++) {
const val = sliderValueMap[(value[1] - i) as keyof typeof sliderValueMap];
result.push(`${val.name}: ${clustersHealthState?.[val.key as keyof ClustersHealthState]}`);
}
setSliderInfo({
value,
desc: result.reverse().join(', '),
});
}
}}
onAfterChange={(value: [number, number]) => {
const result = [];
for (let i = 0; i < value[1] - value[0]; i++) {
const val = sliderValueMap[(value[1] - i) as keyof typeof sliderValueMap];
result.push(val.code);
}
searchParamsChangeFunc.onSilderChange(result);
}}
/>
</div>
</Tooltip>
</div> </div>
</div> </div>
</div> </div>
@@ -269,7 +330,7 @@ const MultiClusterPage = () => {
<div className="multi-cluster-filter-select"> <div className="multi-cluster-filter-select">
<Select <Select
onChange={(value) => searchParamsChangeFunc.onSortInfoChange('sortField', value)} onChange={(value) => searchParamsChangeFunc.onSortInfoChange('sortField', value)}
defaultValue="HealthScore" defaultValue="HealthState"
style={{ width: 170, marginRight: 12 }} style={{ width: 170, marginRight: 12 }}
> >
{sortFieldList.map((item) => ( {sortFieldList.map((item) => (

View File

@@ -10,15 +10,15 @@ import { timeFormat, oneDayMillims } from '@src/constants/common';
import { IMetricPoint, linesMetric, pointsMetric } from './config'; import { IMetricPoint, linesMetric, pointsMetric } from './config';
import { useIntl } from 'react-intl'; import { useIntl } from 'react-intl';
import api, { MetricType } from '@src/api'; import api, { MetricType } from '@src/api';
import { getHealthClassName, getHealthProcessColor, getHealthText } from '../SingleClusterDetail/config';
import { ClustersPermissionMap } from '../CommonConfig'; import { ClustersPermissionMap } from '../CommonConfig';
import { getDataUnit } from '@src/constants/chartConfig'; import { getDataUnit } from '@src/constants/chartConfig';
import SmallChart from '@src/components/SmallChart'; import SmallChart from '@src/components/SmallChart';
import HealthState, { HealthStateEnum } from '@src/components/HealthState';
import { SearchParams } from './HomePage'; import { SearchParams } from './HomePage';
const DEFAULT_PAGE_SIZE = 10; const DEFAULT_PAGE_SIZE = 10;
enum ClusterRunState { export enum ClusterRunState {
Raft = 2, Raft = 2,
} }
@@ -165,12 +165,9 @@ const ClusterList = (props: { searchParams: SearchParams; showAccessCluster: any
fieldName: 'kafkaVersion', fieldName: 'kafkaVersion',
fieldValueList: searchParams.checkedKafkaVersions as (string | number)[], fieldValueList: searchParams.checkedKafkaVersions as (string | number)[],
}, },
],
rangeFilterDTOList: [
{ {
fieldMaxValue: searchParams.healthScoreRange[1], fieldName: 'HealthState',
fieldMinValue: searchParams.healthScoreRange[0], fieldValueList: searchParams.healthState,
fieldName: 'HealthScore',
}, },
], ],
searchKeywords: searchParams.keywords, searchKeywords: searchParams.keywords,
@@ -257,7 +254,7 @@ const ClusterList = (props: { searchParams: SearchParams; showAccessCluster: any
Zookeepers: zks, Zookeepers: zks,
HealthCheckPassed: healthCheckPassed, HealthCheckPassed: healthCheckPassed,
HealthCheckTotal: healthCheckTotal, HealthCheckTotal: healthCheckTotal,
HealthScore: healthScore, HealthState: healthState,
ZookeepersAvailable: zookeepersAvailable, ZookeepersAvailable: zookeepersAvailable,
LoadReBalanceCpu: loadReBalanceCpu, LoadReBalanceCpu: loadReBalanceCpu,
LoadReBalanceDisk: loadReBalanceDisk, LoadReBalanceDisk: loadReBalanceDisk,
@@ -272,28 +269,16 @@ const ClusterList = (props: { searchParams: SearchParams; showAccessCluster: any
history.push(`/cluster/${itemData.id}/cluster`); history.push(`/cluster/${itemData.id}/cluster`);
}} }}
> >
<div className={'multi-cluster-list-item'}> <div className="multi-cluster-list-item">
<div className="multi-cluster-list-item-healthy"> <div className="multi-cluster-list-item-healthy">
<Progress <div className="healthy-box">
type="circle" <HealthState state={healthState} width={70} height={70} />
status={!itemData.alive ? 'exception' : healthScore >= 90 ? 'success' : 'normal'} <div className="healthy-degree">
strokeWidth={4} <span className="healthy-degree-status"></span>
// className={healthScore > 90 ? 'green-circle' : ''} <span className="healthy-degree-proportion">
className={+itemData.alive <= 0 ? 'red-circle' : +healthScore < 90 ? 'blue-circle' : 'green-circle'} {healthCheckPassed}/{healthCheckTotal}
strokeColor={getHealthProcessColor(healthScore, itemData.alive)} </span>
percent={itemData.alive ? healthScore : 100} </div>
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> </div>
<div className="multi-cluster-list-item-right"> <div className="multi-cluster-list-item-right">

View File

@@ -1,3 +1,4 @@
import { HealthStateEnum } from '@src/components/HealthState';
import { FormItemType, IFormItem } from 'knowdesign/es/extend/x-form'; import { FormItemType, IFormItem } from 'knowdesign/es/extend/x-form';
export const bootstrapServersErrCodes = [10, 11, 12]; export const bootstrapServersErrCodes = [10, 11, 12];
@@ -21,8 +22,8 @@ export const sortFieldList = [
value: 'createTime', value: 'createTime',
}, },
{ {
label: '健康', label: '健康状态',
value: 'HealthScore', value: 'HealthState',
}, },
{ {
label: 'Messages', label: 'Messages',
@@ -71,18 +72,41 @@ export const metricNameMap = {
[key: string]: string; [key: string]: string;
}; };
export const sliderValueMap = {
1: {
code: HealthStateEnum.GOOD,
key: 'goodCount',
name: '好',
},
2: {
code: HealthStateEnum.MEDIUM,
key: 'mediumCount',
name: '中',
},
3: {
code: HealthStateEnum.POOR,
key: 'poorCount',
name: '差',
},
4: {
code: HealthStateEnum.DOWN,
key: 'deadCount',
name: 'Down',
},
5: {
code: HealthStateEnum.UNKNOWN,
key: 'unknownCount',
name: 'Unknown',
},
};
export const healthSorceList = { export const healthSorceList = {
0: 0, 0: '',
10: '', 1: '',
20: 20, 2: '中',
30: '', 3: '',
40: 40, 4: 'Down',
50: '', 5: 'Unknown',
60: 60,
70: '',
80: 80,
90: '',
100: 100,
}; };
export interface IMetricPoint { export interface IMetricPoint {

View File

@@ -1,5 +1,17 @@
@error-color: #f46a6a; @error-color: #f46a6a;
.cluster-health-state-tooltip {
.dcloud-tooltip-arrow,
.dcloud-tooltip-inner {
margin-bottom: -10px;
}
.dcloud-tooltip-inner {
padding: 2px 4px;
min-height: 25px;
border-radius: 4px;
}
}
.multi-cluster-page { .multi-cluster-page {
position: absolute; position: absolute;
left: 0; left: 0;
@@ -229,6 +241,9 @@
&-slider { &-slider {
width: 298px; width: 298px;
padding-right: 20px; padding-right: 20px;
.dcloud-slider-mark {
left: -27px;
}
} }
&-title { &-title {
@@ -404,59 +419,29 @@
transition: 0.5s all; transition: 0.5s all;
&-healthy { &-healthy {
display: flex;
align-items: center;
margin-right: 24px; margin-right: 24px;
.dcloud-progress-inner { .healthy-box {
border-radius: 50%; position: relative;
} height: 70px;
.green-circle { margin-top: 2px;
.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 { .healthy-degree {
position: absolute;
bottom: 0;
width: 100%;
font-family: @font-family-bold; font-family: @font-family-bold;
font-size: 12px; font-size: 12px;
color: #495057; color: #495057;
line-height: 15px; line-height: 1;
margin-top: 6px;
text-align: center; text-align: center;
&-status { &-status {
margin-right: 6px; margin-right: 6px;
color: #74788d; color: #74788d;
} }
&-proportion {
}
} }
} }

View File

@@ -6,16 +6,27 @@ import { useIntl } from 'react-intl';
import { getHealthySettingColumn } from './config'; import { getHealthySettingColumn } from './config';
import API from '../../api'; import API from '../../api';
import { useParams } from 'react-router-dom'; import { useParams } from 'react-router-dom';
import notification from '@src/components/Notification';
interface HealthConfig {
dimensionCode: number;
dimensionName: string;
configGroup: string;
configItem: string;
configName: string;
configDesc: string;
value: string;
}
const HealthySetting = React.forwardRef((props: any, ref): JSX.Element => { const HealthySetting = React.forwardRef((props: any, ref): JSX.Element => {
const intl = useIntl(); const intl = useIntl();
const [form] = Form.useForm(); const [form] = Form.useForm();
const { clusterId } = useParams<{ clusterId: string }>();
const [visible, setVisible] = useState(false); const [visible, setVisible] = useState(false);
const [initialValues, setInitialValues] = useState({} as any); const [initialValues, setInitialValues] = useState({} as any);
const [data, setData] = React.useState([]); const [data, setData] = React.useState<HealthConfig[]>([]);
const { clusterId } = useParams<{ clusterId: string }>();
React.useImperativeHandle(ref, () => ({ React.useImperativeHandle(ref, () => ({
setVisible, setVisible,
@@ -23,29 +34,25 @@ const HealthySetting = React.forwardRef((props: any, ref): JSX.Element => {
})); }));
const getHealthconfig = () => { const getHealthconfig = () => {
return Utils.request(API.getClusterHealthyConfigs(+clusterId)).then((res: any) => { return Utils.request(API.getClusterHealthyConfigs(+clusterId)).then((res: HealthConfig[]) => {
const values = {} as any; const values = {} as any;
res.sort((a, b) => a.dimensionCode - b.dimensionCode);
try { try {
res = res.map((item: any) => { res.forEach((item) => {
const itemValue = JSON.parse(item.value); const itemValue = JSON.parse(item.value);
item.weight = itemValue?.weight; const { value, latestMinutes, detectedTimes, amount, ratio } = itemValue;
item.configItemName = value && (values[`value_${item.configItem}`] = value);
item.configItem.indexOf('Group Re-Balance') > -1 latestMinutes && (values[`latestMinutes_${item.configItem}`] = latestMinutes);
? 'ReBalance' detectedTimes && (values[`detectedTimes_${item.configItem}`] = detectedTimes);
: item.configItem.includes('副本未同步') amount && (values[`amount_${item.configItem}`] = amount);
? 'UNDER_REPLICA' ratio && (values[`ratio_${item.configItem}`] = ratio);
: item.configItem;
values[`weight_${item.configItemName}`] = itemValue?.weight;
values[`value_${item.configItemName}`] = itemValue?.value;
values[`latestMinutes_${item.configItemName}`] = itemValue?.latestMinutes;
values[`detectedTimes_${item.configItemName}`] = itemValue?.detectedTimes;
return item;
}); });
} catch (err) { } catch (err) {
// notification.error({
message: '健康项检查规则解析失败',
});
} }
const formItemsValue = { const formItemsValue = {
...initialValues, ...initialValues,
@@ -70,10 +77,11 @@ const HealthySetting = React.forwardRef((props: any, ref): JSX.Element => {
clusterId: +clusterId, clusterId: +clusterId,
value: JSON.stringify({ value: JSON.stringify({
clusterPhyId: +clusterId, clusterPhyId: +clusterId,
detectedTimes: res[`detectedTimes_${item.configItemName}`], detectedTimes: res[`detectedTimes_${item.configItem}`],
latestMinutes: res[`latestMinutes_${item.configItemName}`], latestMinutes: res[`latestMinutes_${item.configItem}`],
weight: res[`weight_${item.configItemName}`], amount: res[`amount_${item.configItem}`],
value: item.configItemName === 'Controller' ? 1 : res[`value_${item.configItemName}`], ratio: res[`ratio_${item.configItem}`],
value: item.configItem === 'Controller' ? 1 : res[`value_${item.configItem}`],
}), }),
valueGroup: item.configGroup, valueGroup: item.configGroup,
valueName: item.configName, valueName: item.configName,
@@ -120,7 +128,7 @@ const HealthySetting = React.forwardRef((props: any, ref): JSX.Element => {
<Form form={form} layout="vertical" onValuesChange={onHandleValuesChange}> <Form form={form} layout="vertical" onValuesChange={onHandleValuesChange}>
<ProTable <ProTable
tableProps={{ tableProps={{
rowKey: 'dimensionCode', rowKey: 'configItem',
showHeader: false, showHeader: false,
dataSource: data, dataSource: data,
columns: getHealthySettingColumn(form, data, clusterId), columns: getHealthySettingColumn(form, data, clusterId),

View File

@@ -1,4 +1,4 @@
import { AppContainer, Divider, Progress, Tooltip, Utils } from 'knowdesign'; import { AppContainer, Divider, Tooltip, Utils } from 'knowdesign';
import { IconFont } from '@knowdesign/icons'; import { IconFont } from '@knowdesign/icons';
import React, { useEffect, useState } from 'react'; import React, { useEffect, useState } from 'react';
import AccessClusters from '../MutliClusterPage/AccessCluster'; import AccessClusters from '../MutliClusterPage/AccessCluster';
@@ -7,8 +7,9 @@ import API from '../../api';
import HealthySetting from './HealthySetting'; import HealthySetting from './HealthySetting';
import CheckDetail from './CheckDetail'; import CheckDetail from './CheckDetail';
import { Link, useHistory, useParams } from 'react-router-dom'; import { Link, useHistory, useParams } from 'react-router-dom';
import { getHealthClassName, getHealthProcessColor, getHealthState, getHealthText, renderToolTipValue } from './config'; import { renderToolTipValue } from './config';
import { ClustersPermissionMap } from '../CommonConfig'; import { ClustersPermissionMap } from '../CommonConfig';
import HealthState, { getHealthStateDesc, getHealthStateEmoji } from '@src/components/HealthState';
const LeftSider = () => { const LeftSider = () => {
const [global] = AppContainer.useGlobalValue(); const [global] = AppContainer.useGlobalValue();
@@ -40,6 +41,7 @@ const LeftSider = () => {
return Utils.post( return Utils.post(
API.getPhyClusterMetrics(+clusterId), API.getPhyClusterMetrics(+clusterId),
[ [
'HealthState',
'HealthScore', 'HealthScore',
'HealthCheckPassed', 'HealthCheckPassed',
'HealthCheckTotal', 'HealthCheckTotal',
@@ -102,23 +104,12 @@ const LeftSider = () => {
<> <>
<div className="left-sider"> <div className="left-sider">
<div className="state-card"> <div className="state-card">
<Progress <HealthState state={clusterMetrics?.HealthState} width={74} height={74} />
type="circle"
status="active"
strokeWidth={4}
strokeColor={getHealthProcessColor(clusterMetrics?.HealthScore, clusterMetrics?.Alive)}
percent={clusterMetrics?.HealthScore ?? '-'}
className={+clusterMetrics.Alive <= 0 ? 'red-circle' : +clusterMetrics?.HealthScore < 90 ? 'blue-circle' : 'green-circle'}
format={() => (
<div className={`healthy-percent ${getHealthClassName(clusterMetrics?.HealthScore, clusterMetrics?.Alive)}`}>
{getHealthText(clusterMetrics?.HealthScore, clusterMetrics?.Alive)}
</div>
)}
width={75}
/>
<div className="healthy-state"> <div className="healthy-state">
<div className="healthy-state-status"> <div className="healthy-state-status">
<span>{getHealthState(clusterMetrics?.HealthScore, clusterMetrics?.Alive)}</span> <span>
{getHealthStateEmoji(clusterMetrics?.HealthState)} {getHealthStateDesc(clusterMetrics?.HealthState)}
</span>
{/* 健康分设置 */} {/* 健康分设置 */}
{global.hasPermission && global.hasPermission(ClustersPermissionMap.CLUSTER_CHANGE_HEALTHY) ? ( {global.hasPermission && global.hasPermission(ClustersPermissionMap.CLUSTER_CHANGE_HEALTHY) ? (
<span <span

View File

@@ -7,30 +7,15 @@ import { IconFont } from '@knowdesign/icons';
import { Link } from 'react-router-dom'; import { Link } from 'react-router-dom';
import { systemKey } from '../../constants/menu'; import { systemKey } from '../../constants/menu';
const statusTxtEmojiMap = {
success: {
emoji: '👍',
txt: '优异',
},
normal: {
emoji: '😊',
txt: '正常',
},
exception: {
emoji: '👻',
txt: '异常',
},
};
export const dimensionMap = { export const dimensionMap = {
'-1': { // '-1': {
label: 'Unknown', // label: 'Unknown',
href: ``, // href: ``,
}, // },
0: { // 0: {
label: 'Cluster', // label: 'Cluster',
href: ``, // href: ``,
}, // },
1: { 1: {
label: 'Broker', label: 'Broker',
href: `/broker`, href: `/broker`,
@@ -43,30 +28,100 @@ export const dimensionMap = {
label: 'ConsumerGroup', label: 'ConsumerGroup',
href: `/consumers`, href: `/consumers`,
}, },
4: {
label: 'Zookeeper',
href: '/zookeeper',
},
} as any; } as any;
export const getHealthState = (value: number, down: number) => { const toLowerCase = (name = '') => {
if (value === undefined) return '-'; const [first, ...rest] = name.split('');
const progressStatus = +down <= 0 ? 'exception' : value >= 90 ? 'success' : 'normal'; return first.toUpperCase() + rest.join('').toLowerCase();
};
const CONFIG_ITEM_DETAIL_DESC = {
Controller: () => {
return '集群 Controller 数等于 1';
},
RequestQueueSize: (valueGroup: any) => {
return `Broker-RequestQueueSize 小于 ${valueGroup?.value}`;
},
NoLeader: (valueGroup: any) => {
return `Topic 无 Leader 数小于 ${valueGroup?.value}`;
},
NetworkProcessorAvgIdlePercent: (valueGroup: any) => {
return `Broker-NetworkProcessorAvgIdlePercent 的 idle 大于 ${valueGroup?.value * 100}%`;
},
UnderReplicaTooLong: (valueGroup: any) => {
return `Topic 小于 ${parseFloat(((valueGroup?.detectedTimes / valueGroup?.latestMinutes) * 100).toFixed(2))}% 周期处于未同步状态`;
},
'Group Re-Balance': (valueGroup: any) => {
return `Consumer Group 小于 ${parseFloat(
((valueGroup?.detectedTimes / valueGroup?.latestMinutes) * 100).toFixed(2)
)}% 周期处于 Re-balance 状态`;
},
BrainSplit: () => {
return `Zookeeper 未脑裂`;
},
OutstandingRequests: (valueGroup: any) => {
return `Zookeeper 请求堆积数小于 ${valueGroup?.ratio * 100}% 总容量`;
},
WatchCount: (valueGroup: any) => {
return `Zookeeper 订阅数小于 ${valueGroup?.ratio * 100}% 总容量`;
},
AliveConnections: (valueGroup: any) => {
return `Zookeeper 连接数小于 ${valueGroup?.ratio * 100}% 总容量`;
},
ApproximateDataSize: (valueGroup: any) => {
return `Zookeeper 数据大小小于 ${valueGroup?.ratio * 100}% 总容量`;
},
SentRate: (valueGroup: any) => {
return `Zookeeper 首发包数小于 ${valueGroup?.ratio * 100}% 总容量`;
},
};
export const getConfigItemDetailDesc = (item: keyof typeof CONFIG_ITEM_DETAIL_DESC, valueGroup: any) => {
return CONFIG_ITEM_DETAIL_DESC[item]?.(valueGroup);
};
const getFormItem = (params: { configItem: string; type?: string; percent?: boolean; attrs?: any; validator?: any }) => {
const { validator, configItem, percent, type = 'value', attrs = { min: 0 } } = params;
return ( return (
<span> <Form.Item
{statusTxtEmojiMap[progressStatus].emoji}&nbsp;{statusTxtEmojiMap[progressStatus].txt} name={`${type}_${configItem}`}
</span> label=""
rules={
validator
? [
{
required: true,
validator: validator,
},
]
: [
{
required: true,
message: '请输入',
},
]
}
>
{percent ? (
<InputNumber
size="small"
min={0}
max={1}
style={{ width: 86 }}
formatter={(value) => `${value * 100}%`}
parser={(value: any) => parseFloat(value.replace('%', '')) / 100}
/>
) : (
<InputNumber style={{ width: 86 }} size="small" {...attrs} />
)}
</Form.Item>
); );
}; };
export const getHealthText = (value: number, down: number) => {
return +down <= 0 ? 'Down' : value ? value.toFixed(0) : '-';
};
export const getHealthProcessColor = (value: number, down: number) => {
return +down <= 0 ? '#FF7066' : +value < 90 ? '#556EE6' : '#00C0A2';
};
export const getHealthClassName = (value: number, down: number) => {
return +down <= 0 ? 'down' : value === undefined ? 'no-info' : +value < 90 ? 'less-90' : '';
};
export const renderToolTipValue = (value: string, num: number) => { export const renderToolTipValue = (value: string, num: number) => {
return ( return (
<> <>
@@ -88,48 +143,40 @@ export const getDetailColumn = (clusterId: number) => [
title: '检查模块', title: '检查模块',
dataIndex: 'dimension', dataIndex: 'dimension',
// eslint-disable-next-line react/display-name // eslint-disable-next-line react/display-name
render: (text: number) => { render: (text: number, record: any) => {
if (text === 0 || text === -1) return dimensionMap[text]?.label; return dimensionMap[text] ? (
return <Link to={`/${systemKey}/${clusterId}${dimensionMap[text]?.href}`}>{dimensionMap[text]?.label}</Link>; <Link to={`/${systemKey}/${clusterId}${dimensionMap[text]?.href}`}>{toLowerCase(record?.dimensionName)}</Link>
) : (
toLowerCase(record?.dimensionName)
);
}, },
}, },
{ {
title: '检查项', title: '检查项',
dataIndex: 'checkConfig', dataIndex: 'checkConfig',
render(config: any, record: any) { render(config: any, record: any) {
const valueGroup = JSON.parse(config.value); let valueGroup = {};
if (record.configItem === 'Controller') { try {
return '集群 Controller 数等于 1'; valueGroup = JSON.parse(config.value);
} else if (record.configItem === 'RequestQueueSize') { } catch (e) {
return `Broker-RequestQueueSize 小于 ${valueGroup.value}`; //
} else if (record.configItem === 'NoLeader') {
return `Topic 无 Leader 数小于 ${valueGroup.value}`;
} else if (record.configItem === 'NetworkProcessorAvgIdlePercent') {
return `Broker-NetworkProcessorAvgIdlePercent 的 idle 大于 ${valueGroup.value}%`;
} else if (record.configItem === 'UnderReplicaTooLong') {
return `Topic 小于 ${parseFloat(((valueGroup.detectedTimes / valueGroup.latestMinutes) * 100).toFixed(2))}% 周期处于未同步状态`;
} else if (record.configItem === 'Group Re-Balance') {
return `Consumer Group 小于 ${parseFloat(
((valueGroup.detectedTimes / valueGroup.latestMinutes) * 100).toFixed(2)
)}% 周期处于 Re-balance 状态`;
} }
return getConfigItemDetailDesc(record.configItem, valueGroup) || record.configDesc || '-';
return <></>;
}, },
}, },
{ // {
title: '权重', // title: '权重',
dataIndex: 'weightPercent', // dataIndex: 'weightPercent',
width: 80, // width: 80,
render(value: number) { // render(value: number) {
return `${value}%`; // return `${value}%`;
}, // },
}, // },
{ // {
title: '得分', // title: '得分',
dataIndex: 'score', // dataIndex: 'score',
width: 60, // width: 60,
}, // },
{ {
title: '检查时间', title: '检查时间',
width: 190, width: 190,
@@ -168,168 +215,160 @@ export const getHealthySettingColumn = (form: any, data: any, clusterId: string)
{ {
title: '检查模块', title: '检查模块',
dataIndex: 'dimensionCode', dataIndex: 'dimensionCode',
width: 140,
// eslint-disable-next-line react/display-name // eslint-disable-next-line react/display-name
render: (text: number) => { render: (text: number, record: any) => {
if (text === 0 || text === -1) return dimensionMap[text]?.label; return dimensionMap[text] ? (
return <Link to={`/${systemKey}/${clusterId}${dimensionMap[text]?.href}`}>{dimensionMap[text]?.label}</Link>; <Link to={`/${systemKey}/${clusterId}${dimensionMap[text]?.href}`}>{toLowerCase(record?.dimensionName)}</Link>
) : (
toLowerCase(record?.dimensionName)
);
}, },
}, },
{ {
title: '检查项', title: '检查项',
dataIndex: 'configItem', dataIndex: 'configItem',
width: 200, width: 230,
needTooltip: true, needTooltip: true,
}, },
{ {
title: '检查项描述', title: '检查项描述',
dataIndex: 'configDesc', dataIndex: 'configDesc',
width: 240, width: 310,
needToolTip: true, needToolTip: true,
}, },
{ // {
title: '权重', // title: '权重',
dataIndex: 'weight', // dataIndex: 'weight',
// width: 180, // // width: 180,
// eslint-disable-next-line react/display-name // // eslint-disable-next-line react/display-name
render: (text: number, record: any) => { // render: (text: number, record: any) => {
return ( // return (
<> // <>
<Form.Item // <Form.Item
name={`weight_${record.configItemName}`} // name={`weight_${record.configItemName}`}
label="" // label=""
rules={[ // rules={[
{ // {
required: true, // required: true,
validator: async (rule: any, value: string) => { // validator: async (rule: any, value: string) => {
const otherWeightCongigName: string[] = []; // const otherWeightCongigName: string[] = [];
let totalPercent = 0; // let totalPercent = 0;
data.map((item: any) => { // data.map((item: any) => {
if (item.configItemName !== record.configItemName) { // if (item.configItemName !== record.configItemName) {
otherWeightCongigName.push(`weight_${item.configItemName}`); // otherWeightCongigName.push(`weight_${item.configItemName}`);
totalPercent += form.getFieldValue(`weight_${item.configItemName}`) ?? 0; // totalPercent += form.getFieldValue(`weight_${item.configItemName}`) ?? 0;
} // }
}); // });
if (!value) { // if (!value) {
return Promise.reject('请输入权重'); // return Promise.reject('请输入权重');
} // }
if (+value < 0) { // if (+value < 0) {
return Promise.reject('最小为0'); // return Promise.reject('最小为0');
} // }
if (+value + totalPercent !== 100) { // if (+value + totalPercent !== 100) {
return Promise.reject('总和应为100%'); // return Promise.reject('总和应为100%');
} // }
form.setFields(otherWeightCongigName.map((i) => ({ name: i, errors: [] }))); // form.setFields(otherWeightCongigName.map((i) => ({ name: i, errors: [] })));
return Promise.resolve(''); // return Promise.resolve('');
}, // },
}, // },
]} // ]}
> // >
<InputNumber // <InputNumber
size="small" // size="small"
min={0} // min={0}
max={100} // max={100}
formatter={(value) => `${value}%`} // formatter={(value) => `${value}%`}
parser={(value: any) => value.replace('%', '')} // parser={(value: any) => value.replace('%', '')}
/> // />
</Form.Item> // </Form.Item>
</> // </>
); // );
}, // },
}, // },
{ {
title: '检查规则', title: '检查规则',
// width: 350, // width: 350,
dataIndex: 'passed', dataIndex: 'passed',
// eslint-disable-next-line react/display-name // eslint-disable-next-line react/display-name
render: (text: any, record: any) => { render: (text: any, record: any) => {
const getFormItem = (params: { type?: string; percent?: boolean; attrs?: any; validator?: any }) => { const configItem = record.configItem;
const { validator, percent, type = 'value', attrs = { min: 0 } } = params;
return (
<Form.Item
name={`${type}_${record.configItemName}`}
label=""
rules={
validator
? [
{
required: true,
validator: validator,
},
]
: [
{
required: true,
message: '请输入',
},
]
}
>
{percent ? (
<InputNumber
size="small"
min={0}
max={1}
style={{ width: 86 }}
formatter={(value) => `${value * 100}%`}
parser={(value: any) => parseFloat(value.replace('%', '')) / 100}
/>
) : (
<InputNumber style={{ width: 86 }} size="small" {...attrs} />
)}
</Form.Item>
);
};
if (record.configItemName === 'Controller') { switch (configItem) {
return <div className="table-form-item"> 1 </div>; case 'Controller': {
} return <div className="table-form-item"> 1 </div>;
if (record.configItemName === 'RequestQueueSize' || record.configItemName === 'NoLeader') { }
return ( case 'BrainSplit': {
<div className="table-form-item"> return <div className="table-form-item"></div>;
<span className="left-text"></span> }
{getFormItem({ attrs: { min: 0, max: 99998 } })} case 'RequestQueueSize':
<span className="right-text"></span> case 'NoLeader': {
</div> return (
); <div className="table-form-item">
} <span className="left-text"></span>
if (record.configItemName === 'NetworkProcessorAvgIdlePercent') { {getFormItem({ configItem, attrs: { min: 0, max: 99998 } })}
return ( <span className="right-text"></span>
<div className="table-form-item"> </div>
<span className="left-text"></span> );
{getFormItem({ percent: true })} }
<span className="right-text"></span> case 'SentRate':
</div> case 'WatchCount':
); case 'AliveConnections':
} case 'ApproximateDataSize':
if (record.configItemName === 'UnderReplicaTooLong' || record.configItemName === 'ReBalance') { case 'OutstandingRequests': {
return ( return (
<div className="table-form-item"> <div className="table-form-item">
{getFormItem({ type: 'latestMinutes', attrs: { min: 1, max: 10080 } })} <span className="left-text"></span>
<span className="right-text left-text"></span> {getFormItem({ configItem, type: 'amount' })}
{getFormItem({ <span className="left-text">, </span>
type: 'detectedTimes', {getFormItem({ configItem, type: 'ratio', percent: true })}
attrs: { min: 1, max: 10080 }, <span className="right-text"></span>
validator: async (rule: any, value: string) => { </div>
const latestMinutesValue = form.getFieldValue(`latestMinutes_${record.configItemName}`); );
}
case 'NetworkProcessorAvgIdlePercent': {
return (
<div className="table-form-item">
<span className="left-text"></span>
{getFormItem({ configItem, percent: true })}
<span className="right-text"></span>
</div>
);
}
case 'UnderReplicaTooLong':
case 'Group Re-Balance': {
return (
<div className="table-form-item">
{getFormItem({ type: 'latestMinutes', configItem, attrs: { min: 1, max: 10080 } })}
<span className="right-text left-text"></span>
{getFormItem({
type: 'detectedTimes',
configItem,
attrs: { min: 1, max: 10080 },
validator: async (rule: any, value: string) => {
const latestMinutesValue = form.getFieldValue(`latestMinutes_${configItem}`);
if (!value) { if (!value) {
return Promise.reject('请输入'); return Promise.reject('请输入');
} }
if (+value < 1) { if (+value < 1) {
return Promise.reject('最小为1'); return Promise.reject('最小为1');
} }
if (+value > +latestMinutesValue) { if (+value > +latestMinutesValue) {
return Promise.reject('值不能大于周期'); return Promise.reject('值不能大于周期');
} }
return Promise.resolve(''); return Promise.resolve('');
}, },
})} })}
<span className="right-text"></span> <span className="right-text"></span>
</div> </div>
); );
}
default: {
return <></>;
}
} }
return <></>;
}, },
}, },
] as any; ] as any;

View File

@@ -195,15 +195,14 @@
} }
.healthy-state { .healthy-state {
margin-left: 14px; margin-left: 10px;
margin-top: 8px;
&-status { &-status {
font-size: 13px; font-size: 13px;
color: #495057; color: #495057;
letter-spacing: 0; letter-spacing: 0;
line-height: 20px; line-height: 20px;
margin-bottom: 13px; margin-bottom: 10px;
.icon { .icon {
margin-left: 4px; margin-left: 4px;
@@ -225,7 +224,7 @@
} }
.dcloud-divider-horizontal { .dcloud-divider-horizontal {
margin: 16px 4px; margin: 0 16px 14px 0;
padding: 0px 20px; padding: 0px 20px;
} }
} }