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

@@ -6,16 +6,27 @@ import { useIntl } from 'react-intl';
import { getHealthySettingColumn } from './config';
import API from '../../api';
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 intl = useIntl();
const [form] = Form.useForm();
const { clusterId } = useParams<{ clusterId: string }>();
const [visible, setVisible] = useState(false);
const [initialValues, setInitialValues] = useState({} as any);
const [data, setData] = React.useState([]);
const { clusterId } = useParams<{ clusterId: string }>();
const [data, setData] = React.useState<HealthConfig[]>([]);
React.useImperativeHandle(ref, () => ({
setVisible,
@@ -23,29 +34,25 @@ const HealthySetting = React.forwardRef((props: any, ref): JSX.Element => {
}));
const getHealthconfig = () => {
return Utils.request(API.getClusterHealthyConfigs(+clusterId)).then((res: any) => {
return Utils.request(API.getClusterHealthyConfigs(+clusterId)).then((res: HealthConfig[]) => {
const values = {} as any;
res.sort((a, b) => a.dimensionCode - b.dimensionCode);
try {
res = res.map((item: any) => {
res.forEach((item) => {
const itemValue = JSON.parse(item.value);
item.weight = itemValue?.weight;
const { value, latestMinutes, detectedTimes, amount, ratio } = itemValue;
item.configItemName =
item.configItem.indexOf('Group Re-Balance') > -1
? 'ReBalance'
: item.configItem.includes('副本未同步')
? 'UNDER_REPLICA'
: 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;
value && (values[`value_${item.configItem}`] = value);
latestMinutes && (values[`latestMinutes_${item.configItem}`] = latestMinutes);
detectedTimes && (values[`detectedTimes_${item.configItem}`] = detectedTimes);
amount && (values[`amount_${item.configItem}`] = amount);
ratio && (values[`ratio_${item.configItem}`] = ratio);
});
} catch (err) {
//
notification.error({
message: '健康项检查规则解析失败',
});
}
const formItemsValue = {
...initialValues,
@@ -70,10 +77,11 @@ const HealthySetting = React.forwardRef((props: any, ref): JSX.Element => {
clusterId: +clusterId,
value: JSON.stringify({
clusterPhyId: +clusterId,
detectedTimes: res[`detectedTimes_${item.configItemName}`],
latestMinutes: res[`latestMinutes_${item.configItemName}`],
weight: res[`weight_${item.configItemName}`],
value: item.configItemName === 'Controller' ? 1 : res[`value_${item.configItemName}`],
detectedTimes: res[`detectedTimes_${item.configItem}`],
latestMinutes: res[`latestMinutes_${item.configItem}`],
amount: res[`amount_${item.configItem}`],
ratio: res[`ratio_${item.configItem}`],
value: item.configItem === 'Controller' ? 1 : res[`value_${item.configItem}`],
}),
valueGroup: item.configGroup,
valueName: item.configName,
@@ -120,7 +128,7 @@ const HealthySetting = React.forwardRef((props: any, ref): JSX.Element => {
<Form form={form} layout="vertical" onValuesChange={onHandleValuesChange}>
<ProTable
tableProps={{
rowKey: 'dimensionCode',
rowKey: 'configItem',
showHeader: false,
dataSource: data,
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 React, { useEffect, useState } from 'react';
import AccessClusters from '../MutliClusterPage/AccessCluster';
@@ -7,8 +7,9 @@ import API from '../../api';
import HealthySetting from './HealthySetting';
import CheckDetail from './CheckDetail';
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 HealthState, { getHealthStateDesc, getHealthStateEmoji } from '@src/components/HealthState';
const LeftSider = () => {
const [global] = AppContainer.useGlobalValue();
@@ -40,6 +41,7 @@ const LeftSider = () => {
return Utils.post(
API.getPhyClusterMetrics(+clusterId),
[
'HealthState',
'HealthScore',
'HealthCheckPassed',
'HealthCheckTotal',
@@ -102,23 +104,12 @@ const LeftSider = () => {
<>
<div className="left-sider">
<div className="state-card">
<Progress
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}
/>
<HealthState state={clusterMetrics?.HealthState} width={74} height={74} />
<div className="healthy-state">
<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) ? (
<span

View File

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

View File

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