Files
KnowStreaming/km-console/packages/layout-clusters-fe/src/pages/SingleClusterDetail/DetailChart/index.tsx
2022-10-21 14:47:00 +08:00

459 lines
18 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

import { Col, Row, SingleChart, Utils, Modal, Spin, Empty, AppContainer, Tooltip } from 'knowdesign';
import { IconFont } from '@knowdesign/icons';
import React, { useEffect, useRef, useState } from 'react';
import { arrayMoveImmutable } from 'array-move';
import api from '@src/api';
import { useParams } from 'react-router-dom';
import {
OriginMetricData,
FormattedMetricData,
formatChartData,
supplementaryPoints,
resolveMetricsRank,
MetricInfo,
} from '@src/constants/chartConfig';
import { MetricType } from '@src/api';
import { getDataUnit } from '@src/constants/chartConfig';
import ChartOperateBar, { KsHeaderOptions } from '@src/components/ChartOperateBar';
import RenderEmpty from '@src/components/RenderEmpty';
import DragGroup from '@src/components/DragGroup';
import { getChartConfig } from './config';
import './index.less';
type ChartFilterOptions = Omit<KsHeaderOptions, 'gridNum'>;
interface MessagesInDefaultData {
aggType: string | null;
createTime: string | null;
updateTime: string | null;
timeStamp: number;
values: {
[metric: string]: string;
};
}
type MessagesInMetric = {
name: 'MessagesIn';
unit: string;
data: (readonly [number, number | string, { key: string; value: number; unit: string }[]])[];
};
const { EventBus } = Utils;
const busInstance = new EventBus();
// 图表颜色定义 & 计算
const CHART_LINE_COLORS = ['#556EE6', '#3991FF'];
const calculateChartColor = (i: number) => {
const isEvenRow = ((i / 2) | 0) % 2;
const isEvenCol = i % 2;
return CHART_LINE_COLORS[isEvenRow ^ isEvenCol];
};
const DEFAULT_METRIC = 'MessagesIn';
// 默认指标图表固定需要获取展示的指标项
const DEFUALT_METRIC_NEED_METRICS = [DEFAULT_METRIC, 'TotalLogSize', 'TotalProduceRequests', 'Topics', 'Partitions'];
const DetailChart = (props: { children: JSX.Element }): JSX.Element => {
const [global] = AppContainer.useGlobalValue();
const { clusterId } = useParams<{ clusterId: string }>();
const [metricList, setMetricList] = useState<MetricInfo[]>([]); // 指标列表
const [selectedMetricNames, setSelectedMetricNames] = useState<(string | number)[]>([]); // 默认选中的指标的列表
const [metricDataList, setMetricDataList] = useState<any>([]);
const [messagesInMetricData, setMessagesInMetricData] = useState<MessagesInMetric>({
name: 'MessagesIn',
unit: '',
data: undefined,
});
const [curHeaderOptions, setCurHeaderOptions] = useState<ChartFilterOptions>();
const [defaultChartLoading, setDefaultChartLoading] = useState<boolean>(true);
const [chartLoading, setChartLoading] = useState<boolean>(true);
const metricRankList = useRef<string[]>([]);
const curFetchingTimestamp = useRef({
messagesIn: 0,
other: 0,
});
// 筛选项变化或者点击刷新按钮
const ksHeaderChange = (ksOptions: KsHeaderOptions) => {
// 如果为相对时间,则当前时间减去 1 分钟,避免最近一分钟的数据还没采集到时前端多补一个点
if (ksOptions.isRelativeRangeTime) {
ksOptions.rangeTime = ksOptions.rangeTime.map((timestamp) => timestamp - 60 * 1000) as [number, number];
}
setCurHeaderOptions({
isRelativeRangeTime: ksOptions.isRelativeRangeTime,
isAutoReload: ksOptions.isAutoReload,
rangeTime: ksOptions.rangeTime,
});
};
// 更新 rank
const updateRank = (metricList: MetricInfo[]) => {
const { list, listInfo, shouldUpdate } = resolveMetricsRank(metricList);
metricRankList.current = list;
if (shouldUpdate) {
updateMetricList(listInfo);
}
};
// 获取指标列表
const getMetricList = () => {
Utils.request(api.getDashboardMetricList(clusterId, MetricType.Cluster)).then((res: MetricInfo[] | null) => {
if (!res) return;
const supportMetrics = res.filter((metric) => metric.support);
const selectedMetrics = supportMetrics.filter((metric) => metric.set).map((metric) => metric.name);
!selectedMetrics.includes(DEFAULT_METRIC) && selectedMetrics.push(DEFAULT_METRIC);
updateRank([...supportMetrics]);
setMetricList(supportMetrics);
setSelectedMetricNames(selectedMetrics);
});
};
// 更新指标
const updateMetricList = (metricDetailDTOList: { metric: string; rank: number; set: boolean }[]) => {
return Utils.request(api.getDashboardMetricList(clusterId, MetricType.Cluster), {
method: 'POST',
data: {
metricDetailDTOList,
},
});
};
// 指标选中项更新回调
const indicatorChangeCallback = (newMetricNames: (string | number)[]) => {
const updateMetrics: { metric: string; set: boolean; rank: number }[] = [];
// 需要选中的指标
newMetricNames.forEach(
(name) =>
!selectedMetricNames.includes(name) &&
updateMetrics.push({ metric: name as string, set: true, rank: metricList.find(({ name: metric }) => metric === name)?.rank })
);
// 取消选中的指标
selectedMetricNames.forEach(
(name) =>
!newMetricNames.includes(name) &&
updateMetrics.push({ metric: name as string, set: false, rank: metricList.find(({ name: metric }) => metric === name)?.rank })
);
const requestPromise = Object.keys(updateMetrics).length ? updateMetricList(updateMetrics) : Promise.resolve();
requestPromise.then(
() => getMetricList(),
() => getMetricList()
);
return requestPromise;
};
// 获取 metric 列表的图表数据
const getMetricData = () => {
if (!selectedMetricNames.length) return;
!curHeaderOptions.isAutoReload && setChartLoading(true);
const [startTime, endTime] = curHeaderOptions.rangeTime;
const curTimestamp = Date.now();
curFetchingTimestamp.current = {
...curFetchingTimestamp.current,
messagesIn: curTimestamp,
};
Utils.request(api.getClusterMetricDataList(), {
method: 'POST',
data: {
startTime,
endTime,
clusterPhyIds: [clusterId],
metricsNames: selectedMetricNames.filter((name) => name !== DEFAULT_METRIC),
},
}).then(
(res: OriginMetricData[]) => {
// 如果当前请求不是最新请求,则不做任何操作
if (curFetchingTimestamp.current.messagesIn !== curTimestamp) {
return;
}
const formattedMetricData: FormattedMetricData[] = formatChartData(
res,
global.getMetricDefine || {},
MetricType.Cluster,
curHeaderOptions.rangeTime
);
formattedMetricData.forEach((data) => (data.metricLines[0].name = data.metricName));
// 指标排序
formattedMetricData.sort((a, b) => metricRankList.current.indexOf(a.metricName) - metricRankList.current.indexOf(b.metricName));
setMetricDataList(formattedMetricData);
setChartLoading(false);
},
() => setChartLoading(false)
);
};
// 获取默认展示指标的图表数据
const getDefaultMetricData = () => {
!curHeaderOptions.isAutoReload && setDefaultChartLoading(true);
const curTimestamp = Date.now();
curFetchingTimestamp.current = {
...curFetchingTimestamp.current,
other: curTimestamp,
};
Utils.request(api.getClusterDefaultMetricData(), {
method: 'POST',
data: {
startTime: curHeaderOptions.rangeTime[0],
endTime: curHeaderOptions.rangeTime[1],
clusterPhyIds: [clusterId],
metricsNames: DEFUALT_METRIC_NEED_METRICS,
},
}).then(
(res: MessagesInDefaultData[]) => {
// 如果当前请求不是最新请求,则不做任何操作
if (curFetchingTimestamp.current.other !== curTimestamp) {
return;
}
// TODO: 这里直接将指标数据放到数组第三项中,之后可以尝试优化,优化需要注意 tooltipFormatter 函数也要修改
let maxValue = -1;
const result = res.map((item) => {
const { timeStamp, values } = item;
let parsedValue: string | number = Number(values.MessagesIn);
if (Number.isNaN(parsedValue)) {
parsedValue = values.MessagesIn;
} else {
// 为避免出现过小的数字影响图表展示效果,图表值统一只保留到小数点后三位
parsedValue = parseFloat(parsedValue.toFixed(3));
if (maxValue < parsedValue) maxValue = parsedValue;
}
const valuesWithUnit = Object.entries(values).map(([key, value]) => {
let valueWithUnit = Number(value);
let unit = ((global.getMetricDefine && global.getMetricDefine(MetricType.Cluster, key)?.unit) || '') as string;
if (unit.toLowerCase().includes('byte')) {
const [unitName, unitSize]: [string, number] = getDataUnit['Memory'](Number(value));
unit = unit.toLowerCase().replace('byte', unitName);
valueWithUnit /= unitSize;
}
const returnValue = {
key,
value: valueWithUnit,
unit,
};
return returnValue;
});
return [timeStamp, parsedValue || '0', valuesWithUnit] as [number, number | string, typeof valuesWithUnit];
});
result.sort((a, b) => (a[0] as number) - (b[0] as number));
const line = {
name: 'MessagesIn' as const,
unit: global.getMetricDefine(MetricType.Cluster, 'MessagesIn')?.unit,
data: result as any,
};
if (maxValue > 0) {
const [unitName, unitSize]: [string, number] = getDataUnit['Num'](maxValue);
line.unit = `${unitName}${line.unit}`;
result.forEach((point) => parseFloat(((point[1] as number) /= unitSize).toFixed(3)));
}
if (result.length) {
// 补充缺少的图表点
const extraMetrics = result[0][2].map((info) => ({
...info,
value: 0,
}));
supplementaryPoints([line], curHeaderOptions.rangeTime, (point) => {
point.push(extraMetrics as any);
return point;
});
}
setMessagesInMetricData(line);
setDefaultChartLoading(false);
},
() => setDefaultChartLoading(false)
);
};
// 监听盒子宽度变化,重置图表宽度
const observeDashboardWidthChange = () => {
const targetNode = document.getElementsByClassName('dcd-two-columns-layout-sider-footer')[0];
targetNode && targetNode.addEventListener('click', () => busInstance.emit('chartResize'));
};
// 拖拽开始回调,触发图表的 onDrag 事件( 设置为 true ),禁止同步展示图表的 tooltip
const dragStart = () => {
busInstance.emit('onDrag', true);
};
// 拖拽结束回调,更新图表顺序,并触发图表的 onDrag 事件( 设置为 false ),允许同步展示图表的 tooltip
const dragEnd = ({ oldIndex, newIndex }: { oldIndex: number; newIndex: number }) => {
busInstance.emit('onDrag', false);
const originFrom = metricRankList.current.indexOf(metricDataList[oldIndex].metricName);
const originTarget = metricRankList.current.indexOf(metricDataList[newIndex].metricName);
const newList = arrayMoveImmutable(metricRankList.current, originFrom, originTarget);
metricRankList.current = newList;
updateMetricList(newList.map((metric, rank) => ({ metric, rank, set: metricList.find(({ name }) => metric === name)?.set || false })));
setMetricDataList(arrayMoveImmutable(metricDataList, oldIndex, newIndex));
};
useEffect(() => {
getMetricData();
}, [selectedMetricNames]);
useEffect(() => {
if (curHeaderOptions && curHeaderOptions?.rangeTime.join(',') !== '0,0') {
getDefaultMetricData();
getMetricData();
}
}, [curHeaderOptions]);
useEffect(() => {
getMetricList();
setTimeout(() => observeDashboardWidthChange());
}, []);
return (
<div className="chart-panel cluster-detail-container">
<ChartOperateBar
onChange={ksHeaderChange}
hideNodeScope={true}
hideGridSelect={true}
metricSelect={{
hide: false,
metricType: MetricType.Cluster,
tableData: metricList,
selectedRows: selectedMetricNames,
checkboxProps: (record: MetricInfo) => {
return record.name === DEFAULT_METRIC
? {
disabled: true,
}
: {};
},
submitCallback: indicatorChangeCallback,
}}
/>
<div className="cluster-detail-container-main">
{/* MessageIn 图表 */}
<div className="header-chart-container">
<Spin spinning={defaultChartLoading}>
{messagesInMetricData.data && (
<>
<div className="cluster-detail-chart-box-title">
<Tooltip
placement="topLeft"
title={() => {
let content = '';
const metricDefine = global.getMetricDefine(MetricType.Cluster, messagesInMetricData.name);
if (metricDefine) {
content = metricDefine.desc;
}
return content;
}}
>
<span>
<span className="name">{messagesInMetricData.name}</span>
<span className="unit">{messagesInMetricData.unit}</span>
</span>
</Tooltip>
</div>
{messagesInMetricData.data.length ? (
<SingleChart
chartKey="messagesIn"
chartTypeProp="line"
showHeader={false}
wrapStyle={{
width: 'auto',
height: 210,
}}
connectEventName="clusterChart"
eventBus={busInstance}
propChartData={[messagesInMetricData]}
{...getChartConfig({
lineColor: CHART_LINE_COLORS[0],
isDefaultMetric: true,
})}
/>
) : (
!defaultChartLoading && <RenderEmpty message="暂无数据" height={200} />
)}
</>
)}
</Spin>
</div>
<div className="content">
{/* 其余指标图表 */}
<div className="multiple-chart-container">
<div className={!metricDataList.length ? 'multiple-chart-container-loading' : ''}>
<Spin spinning={chartLoading}>
{metricDataList.length ? (
<div className="no-group-con">
<DragGroup
sortableContainerProps={{
onSortStart: dragStart,
onSortEnd: dragEnd,
axis: 'xy',
useDragHandle: true,
}}
gridProps={{
span: 12,
gutter: [16, 16],
}}
>
{metricDataList.map((data: any, i: number) => {
const { metricName, metricUnit, metricLines } = data;
return (
<div key={metricName} className="cluster-detail-chart-box">
<div className="cluster-detail-chart-box-title">
<Tooltip
placement="topLeft"
title={() => {
let content = '';
const metricDefine = global.getMetricDefine(MetricType.Cluster, metricName);
if (metricDefine) {
content = metricDefine.desc;
}
return content;
}}
>
<span>
<span className="name">{metricName}</span>
<span className="unit">{metricUnit}</span>
</span>
</Tooltip>
</div>
<SingleChart
chartKey={metricName}
showHeader={false}
chartTypeProp="line"
wrapStyle={{
width: 'auto',
height: 210,
}}
connectEventName="clusterChart"
eventBus={busInstance}
propChartData={metricLines}
{...getChartConfig({
metricName: `${metricName}{unit|${metricUnit}}`,
lineColor: calculateChartColor(i),
})}
/>
</div>
);
})}
</DragGroup>
</div>
) : chartLoading ? (
<></>
) : (
<RenderEmpty message="请先选择指标或刷新" />
)}
</Spin>
</div>
</div>
{/* 历史配置变更记录内容 */}
<div className="config-change-records-container">{props.children}</div>
</div>
</div>
</div>
);
};
export default DetailChart;