mirror of
https://github.com/didi/KnowStreaming.git
synced 2026-01-06 05:22:16 +08:00
feat: 健康状态展示优化
This commit is contained in:
@@ -1,14 +1,14 @@
|
||||
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 API from '@src/api';
|
||||
import TourGuide, { MultiPageSteps } from '@src/components/TourGuide';
|
||||
import './index.less';
|
||||
import { healthSorceList, sortFieldList, sortTypes, statusFilters } from './config';
|
||||
import { healthSorceList, sliderValueMap, sortFieldList, sortTypes, statusFilters } from './config';
|
||||
import ClusterList from './List';
|
||||
import AccessClusters from './AccessCluster';
|
||||
import CustomCheckGroup from './CustomCheckGroup';
|
||||
import { ClustersPermissionMap } from '../CommonConfig';
|
||||
import './index.less';
|
||||
|
||||
const CheckboxGroup = Checkbox.Group;
|
||||
const { Option } = Select;
|
||||
@@ -19,8 +19,17 @@ interface ClustersState {
|
||||
total: number;
|
||||
}
|
||||
|
||||
interface ClustersHealthState {
|
||||
deadCount: number;
|
||||
goodCount: number;
|
||||
mediumCount: number;
|
||||
poorCount: number;
|
||||
total: number;
|
||||
unknownCount: number;
|
||||
}
|
||||
|
||||
export interface SearchParams {
|
||||
healthScoreRange?: [number, number];
|
||||
healthState?: number[];
|
||||
checkedKafkaVersions?: string[];
|
||||
sortInfo?: {
|
||||
sortField: string;
|
||||
@@ -74,16 +83,24 @@ const MultiClusterPage = () => {
|
||||
liveCount: 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 状态变化导致获取两次列表数据的问题
|
||||
const [searchParams, setSearchParams] = React.useState<SearchParams>({
|
||||
keywords: '',
|
||||
checkedKafkaVersions: [],
|
||||
healthScoreRange: [0, 100],
|
||||
sortInfo: {
|
||||
sortField: 'HealthScore',
|
||||
sortField: 'HealthState',
|
||||
sortType: 'asc',
|
||||
},
|
||||
clusterStatus: [0, 1],
|
||||
healthState: [-1, 0, 1, 2, 3],
|
||||
// 是否拉取当前所有数据
|
||||
isReloadAll: false,
|
||||
});
|
||||
@@ -91,10 +108,28 @@ const MultiClusterPage = () => {
|
||||
const searchKeyword = useRef('');
|
||||
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 = () => {
|
||||
getPhyClusterHealthState();
|
||||
Utils.request(API.phyClusterState)
|
||||
.then((res: any) => {
|
||||
.then((res: ClustersState) => {
|
||||
setStateInfo(res);
|
||||
})
|
||||
.finally(() => {
|
||||
@@ -111,13 +146,14 @@ const MultiClusterPage = () => {
|
||||
|
||||
const updateSearchParams = (params: SearchParams) => {
|
||||
setSearchParams((curParams) => ({ ...curParams, isReloadAll: false, ...params }));
|
||||
getPhyClusterHealthState();
|
||||
};
|
||||
|
||||
const searchParamsChangeFunc = {
|
||||
// 健康分改变
|
||||
onSilderChange: (value: [number, number]) =>
|
||||
onSilderChange: (value: number[]) =>
|
||||
updateSearchParams({
|
||||
healthScoreRange: value,
|
||||
healthState: value,
|
||||
}),
|
||||
// 排序信息改变
|
||||
onSortInfoChange: (type: string, value: string) =>
|
||||
@@ -251,16 +287,41 @@ const MultiClusterPage = () => {
|
||||
</div>
|
||||
</div>
|
||||
<div className="header-filter-bottom-item header-filter-bottom-item-slider">
|
||||
<h3 className="header-filter-bottom-item-title title-right">健康分</h3>
|
||||
<div className="header-filter-bottom-item-content">
|
||||
<Slider
|
||||
range
|
||||
step={20}
|
||||
defaultValue={[0, 100]}
|
||||
marks={healthSorceList}
|
||||
onAfterChange={searchParamsChangeFunc.onSilderChange}
|
||||
/>
|
||||
</div>
|
||||
<h3 className="header-filter-bottom-item-title title-right">健康状态</h3>
|
||||
<Tooltip title={sliderInfo.desc} overlayClassName="cluster-health-state-tooltip">
|
||||
<div className="header-filter-bottom-item-content" id="clusters-slider">
|
||||
<Slider
|
||||
dots
|
||||
range={{ draggableTrack: true }}
|
||||
step={1}
|
||||
max={5}
|
||||
marks={healthSorceList}
|
||||
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>
|
||||
@@ -269,7 +330,7 @@ const MultiClusterPage = () => {
|
||||
<div className="multi-cluster-filter-select">
|
||||
<Select
|
||||
onChange={(value) => searchParamsChangeFunc.onSortInfoChange('sortField', value)}
|
||||
defaultValue="HealthScore"
|
||||
defaultValue="HealthState"
|
||||
style={{ width: 170, marginRight: 12 }}
|
||||
>
|
||||
{sortFieldList.map((item) => (
|
||||
|
||||
@@ -10,15 +10,15 @@ import { timeFormat, oneDayMillims } from '@src/constants/common';
|
||||
import { IMetricPoint, linesMetric, pointsMetric } from './config';
|
||||
import { useIntl } from 'react-intl';
|
||||
import api, { MetricType } from '@src/api';
|
||||
import { getHealthClassName, getHealthProcessColor, getHealthText } from '../SingleClusterDetail/config';
|
||||
import { ClustersPermissionMap } from '../CommonConfig';
|
||||
import { getDataUnit } from '@src/constants/chartConfig';
|
||||
import SmallChart from '@src/components/SmallChart';
|
||||
import HealthState, { HealthStateEnum } from '@src/components/HealthState';
|
||||
import { SearchParams } from './HomePage';
|
||||
|
||||
const DEFAULT_PAGE_SIZE = 10;
|
||||
|
||||
enum ClusterRunState {
|
||||
export enum ClusterRunState {
|
||||
Raft = 2,
|
||||
}
|
||||
|
||||
@@ -165,12 +165,9 @@ const ClusterList = (props: { searchParams: SearchParams; showAccessCluster: any
|
||||
fieldName: 'kafkaVersion',
|
||||
fieldValueList: searchParams.checkedKafkaVersions as (string | number)[],
|
||||
},
|
||||
],
|
||||
rangeFilterDTOList: [
|
||||
{
|
||||
fieldMaxValue: searchParams.healthScoreRange[1],
|
||||
fieldMinValue: searchParams.healthScoreRange[0],
|
||||
fieldName: 'HealthScore',
|
||||
fieldName: 'HealthState',
|
||||
fieldValueList: searchParams.healthState,
|
||||
},
|
||||
],
|
||||
searchKeywords: searchParams.keywords,
|
||||
@@ -257,7 +254,7 @@ const ClusterList = (props: { searchParams: SearchParams; showAccessCluster: any
|
||||
Zookeepers: zks,
|
||||
HealthCheckPassed: healthCheckPassed,
|
||||
HealthCheckTotal: healthCheckTotal,
|
||||
HealthScore: healthScore,
|
||||
HealthState: healthState,
|
||||
ZookeepersAvailable: zookeepersAvailable,
|
||||
LoadReBalanceCpu: loadReBalanceCpu,
|
||||
LoadReBalanceDisk: loadReBalanceDisk,
|
||||
@@ -272,28 +269,16 @@ const ClusterList = (props: { searchParams: SearchParams; showAccessCluster: any
|
||||
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">
|
||||
<Progress
|
||||
type="circle"
|
||||
status={!itemData.alive ? 'exception' : healthScore >= 90 ? 'success' : 'normal'}
|
||||
strokeWidth={4}
|
||||
// className={healthScore > 90 ? 'green-circle' : ''}
|
||||
className={+itemData.alive <= 0 ? 'red-circle' : +healthScore < 90 ? 'blue-circle' : 'green-circle'}
|
||||
strokeColor={getHealthProcessColor(healthScore, itemData.alive)}
|
||||
percent={itemData.alive ? healthScore : 100}
|
||||
format={() => (
|
||||
<div className={`healthy-percent ${getHealthClassName(healthScore, itemData?.alive)}`}>
|
||||
{getHealthText(healthScore, itemData?.alive)}
|
||||
</div>
|
||||
)}
|
||||
width={70}
|
||||
/>
|
||||
<div className="healthy-degree">
|
||||
<span className="healthy-degree-status">通过</span>
|
||||
<span className="healthy-degree-proportion">
|
||||
{healthCheckPassed}/{healthCheckTotal}
|
||||
</span>
|
||||
<div className="healthy-box">
|
||||
<HealthState state={healthState} width={70} height={70} />
|
||||
<div className="healthy-degree">
|
||||
<span className="healthy-degree-status">通过</span>
|
||||
<span className="healthy-degree-proportion">
|
||||
{healthCheckPassed}/{healthCheckTotal}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="multi-cluster-list-item-right">
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import { HealthStateEnum } from '@src/components/HealthState';
|
||||
import { FormItemType, IFormItem } from 'knowdesign/es/extend/x-form';
|
||||
|
||||
export const bootstrapServersErrCodes = [10, 11, 12];
|
||||
@@ -21,8 +22,8 @@ export const sortFieldList = [
|
||||
value: 'createTime',
|
||||
},
|
||||
{
|
||||
label: '健康分',
|
||||
value: 'HealthScore',
|
||||
label: '健康状态',
|
||||
value: 'HealthState',
|
||||
},
|
||||
{
|
||||
label: 'Messages',
|
||||
@@ -71,18 +72,41 @@ export const metricNameMap = {
|
||||
[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 = {
|
||||
0: 0,
|
||||
10: '',
|
||||
20: 20,
|
||||
30: '',
|
||||
40: 40,
|
||||
50: '',
|
||||
60: 60,
|
||||
70: '',
|
||||
80: 80,
|
||||
90: '',
|
||||
100: 100,
|
||||
0: '',
|
||||
1: '好',
|
||||
2: '中',
|
||||
3: '差',
|
||||
4: 'Down',
|
||||
5: 'Unknown',
|
||||
};
|
||||
|
||||
export interface IMetricPoint {
|
||||
|
||||
@@ -1,5 +1,17 @@
|
||||
@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 {
|
||||
position: absolute;
|
||||
left: 0;
|
||||
@@ -229,6 +241,9 @@
|
||||
&-slider {
|
||||
width: 298px;
|
||||
padding-right: 20px;
|
||||
.dcloud-slider-mark {
|
||||
left: -27px;
|
||||
}
|
||||
}
|
||||
|
||||
&-title {
|
||||
@@ -404,59 +419,29 @@
|
||||
transition: 0.5s all;
|
||||
|
||||
&-healthy {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
margin-right: 24px;
|
||||
.dcloud-progress-inner {
|
||||
border-radius: 50%;
|
||||
}
|
||||
.green-circle {
|
||||
.dcloud-progress-inner {
|
||||
background: #f5fdfc;
|
||||
}
|
||||
}
|
||||
.red-circle {
|
||||
.dcloud-progress-inner {
|
||||
background: #fffafa;
|
||||
}
|
||||
}
|
||||
.healthy-percent {
|
||||
font-family: DIDIFD-Regular;
|
||||
font-size: 40px;
|
||||
text-align: center;
|
||||
line-height: 36px;
|
||||
color: #00c0a2;
|
||||
|
||||
&.less-90 {
|
||||
color: @primary-color;
|
||||
}
|
||||
|
||||
&.no-info {
|
||||
color: #e9e7e7;
|
||||
}
|
||||
|
||||
&.down {
|
||||
font-family: @font-family-bold;
|
||||
font-size: 22px;
|
||||
color: #ff7066;
|
||||
text-align: center;
|
||||
line-height: 30px;
|
||||
}
|
||||
.healthy-box {
|
||||
position: relative;
|
||||
height: 70px;
|
||||
margin-top: 2px;
|
||||
}
|
||||
|
||||
.healthy-degree {
|
||||
position: absolute;
|
||||
bottom: 0;
|
||||
width: 100%;
|
||||
font-family: @font-family-bold;
|
||||
font-size: 12px;
|
||||
color: #495057;
|
||||
line-height: 15px;
|
||||
margin-top: 6px;
|
||||
line-height: 1;
|
||||
text-align: center;
|
||||
|
||||
&-status {
|
||||
margin-right: 6px;
|
||||
color: #74788d;
|
||||
}
|
||||
|
||||
&-proportion {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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),
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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} 集群状态{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;
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user