mirror of
https://github.com/didi/KnowStreaming.git
synced 2026-01-07 15:12:14 +08:00
455 lines
17 KiB
TypeScript
455 lines
17 KiB
TypeScript
import { Col, Row, SingleChart, IconFont, Utils, Modal, Spin, Empty, AppContainer, Tooltip } from 'knowdesign';
|
||
import React, { useEffect, useRef, useState } from 'react';
|
||
import api from '@src/api';
|
||
import { getChartConfig } from './config';
|
||
import './index.less';
|
||
import { useParams } from 'react-router-dom';
|
||
import {
|
||
MetricDefaultChartDataType,
|
||
MetricChartDataType,
|
||
formatChartData,
|
||
supplementaryPoints,
|
||
} from '@src/components/DashboardDragChart/config';
|
||
import { MetricType } from '@src/api';
|
||
import { getDataNumberUnit, getUnit } from '@src/constants/chartConfig';
|
||
import SingleChartHeader, { KsHeaderOptions } from '@src/components/SingleChartHeader';
|
||
import { MAX_TIME_RANGE_WITH_SMALL_POINT_INTERVAL } from '@src/constants/common';
|
||
|
||
type ChartFilterOptions = Omit<KsHeaderOptions, 'gridNum'>;
|
||
interface MetricInfo {
|
||
type: number;
|
||
name: string;
|
||
desc: string;
|
||
set: boolean;
|
||
support: boolean;
|
||
}
|
||
|
||
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: [],
|
||
});
|
||
const [curHeaderOptions, setCurHeaderOptions] = useState<ChartFilterOptions>();
|
||
const [defaultChartLoading, setDefaultChartLoading] = useState<boolean>(true);
|
||
const [chartLoading, setChartLoading] = useState<boolean>(true);
|
||
const [showChartDetailModal, setShowChartDetailModal] = useState<boolean>(false);
|
||
const [chartDetail, setChartDetail] = useState<any>();
|
||
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,
|
||
});
|
||
};
|
||
|
||
// 获取指标列表
|
||
const getMetricList = () => {
|
||
Utils.request(api.getDashboardMetricList(clusterId, MetricType.Cluster)).then((res: MetricInfo[] | null) => {
|
||
if (!res) return;
|
||
const showMetrics = res.filter((metric) => metric.support);
|
||
const selectedMetrics = showMetrics.filter((metric) => metric.set).map((metric) => metric.name);
|
||
!selectedMetrics.includes(DEFAULT_METRIC) && selectedMetrics.push(DEFAULT_METRIC);
|
||
setMetricList(showMetrics);
|
||
setSelectedMetricNames(selectedMetrics);
|
||
});
|
||
};
|
||
|
||
// 更新指标
|
||
const updateMetricList = (metricsSet: { [name: string]: boolean }) => {
|
||
return Utils.request(api.getDashboardMetricList(clusterId, MetricType.Cluster), {
|
||
method: 'POST',
|
||
data: {
|
||
metricsSet,
|
||
},
|
||
});
|
||
};
|
||
|
||
// 指标选中项更新回调
|
||
const indicatorChangeCallback = (newMetricNames: (string | number)[]) => {
|
||
const updateMetrics: { [name: string]: boolean } = {};
|
||
// 需要选中的指标
|
||
newMetricNames.forEach((name) => !selectedMetricNames.includes(name) && (updateMetrics[name] = true));
|
||
// 取消选中的指标
|
||
selectedMetricNames.forEach((name) => !newMetricNames.includes(name) && (updateMetrics[name] = false));
|
||
|
||
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: MetricDefaultChartDataType[]) => {
|
||
// 如果当前请求不是最新请求,则不做任何操作
|
||
if (curFetchingTimestamp.current.messagesIn !== curTimestamp) {
|
||
return;
|
||
}
|
||
|
||
const supplementaryInterval = (endTime - startTime > MAX_TIME_RANGE_WITH_SMALL_POINT_INTERVAL ? 10 : 1) * 60 * 1000;
|
||
const formattedMetricData: MetricChartDataType[] = formatChartData(
|
||
res,
|
||
global.getMetricDefine || {},
|
||
MetricType.Cluster,
|
||
curHeaderOptions.rangeTime,
|
||
supplementaryInterval
|
||
);
|
||
formattedMetricData.forEach((data) => (data.metricLines[0].name = data.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] = getUnit(Number(value));
|
||
unit = unit.toLowerCase().replace('byte', unitName);
|
||
valueWithUnit /= unitSize;
|
||
}
|
||
const returnValue = {
|
||
key,
|
||
value: valueWithUnit,
|
||
unit,
|
||
};
|
||
return returnValue;
|
||
});
|
||
return [timeStamp, values.MessagesIn || '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] = getDataNumberUnit(maxValue);
|
||
line.unit = `${unitName}${line.unit}`;
|
||
result.forEach((point) => ((point[1] as number) /= unitSize));
|
||
}
|
||
|
||
// 补充缺少的图表点
|
||
const extraMetrics = result[0][2].map((info) => ({
|
||
...info,
|
||
value: 0,
|
||
}));
|
||
const supplementaryInterval =
|
||
(curHeaderOptions.rangeTime[1] - curHeaderOptions.rangeTime[0] > MAX_TIME_RANGE_WITH_SMALL_POINT_INTERVAL ? 10 : 1) * 60 * 1000;
|
||
supplementaryPoints([line], curHeaderOptions.rangeTime, supplementaryInterval, (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'));
|
||
};
|
||
|
||
useEffect(() => {
|
||
getMetricData();
|
||
}, [selectedMetricNames]);
|
||
|
||
useEffect(() => {
|
||
if (curHeaderOptions && curHeaderOptions?.rangeTime.join(',') !== '0,0') {
|
||
getDefaultMetricData();
|
||
getMetricData();
|
||
}
|
||
}, [curHeaderOptions]);
|
||
|
||
useEffect(() => {
|
||
getMetricList();
|
||
setTimeout(() => observeDashboardWidthChange());
|
||
}, []);
|
||
|
||
return (
|
||
<div className="cluster-detail-container">
|
||
<SingleChartHeader
|
||
onChange={ksHeaderChange}
|
||
hideNodeScope={true}
|
||
hideGridSelect={true}
|
||
indicatorSelectModule={{
|
||
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 ${!messagesInMetricData.data.length ? 'header-chart-container-loading' : ''}`}>
|
||
<Spin spinning={defaultChartLoading}>
|
||
{/* TODO: 暂时通过判断是否有图表数据来修复,有时间可以查找下宽度溢出的原因 */}
|
||
{messagesInMetricData.data.length ? (
|
||
<>
|
||
<div className="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>
|
||
<SingleChart
|
||
chartKey="messagesIn"
|
||
chartTypeProp="line"
|
||
showHeader={false}
|
||
wrapStyle={{
|
||
width: 'auto',
|
||
height: 210,
|
||
}}
|
||
connectEventName="clusterChart"
|
||
eventBus={busInstance}
|
||
propChartData={[messagesInMetricData]}
|
||
{...getChartConfig({
|
||
// metricName: `${messagesInMetricData.name}{unit|(${messagesInMetricData.unit})}`,
|
||
lineColor: CHART_LINE_COLORS[0],
|
||
isDefaultMetric: true,
|
||
})}
|
||
/>
|
||
</>
|
||
) : (
|
||
''
|
||
)}
|
||
</Spin>
|
||
</div>
|
||
|
||
<div className="content">
|
||
{/* 其余指标图表 */}
|
||
<div className="multiple-chart-container">
|
||
<div className={!metricDataList.length ? 'multiple-chart-container-loading' : ''}>
|
||
<Spin spinning={chartLoading}>
|
||
<Row gutter={[16, 16]}>
|
||
{metricDataList.length ? (
|
||
metricDataList.map((data: any, i: number) => {
|
||
const { metricName, metricUnit, metricLines } = data;
|
||
return (
|
||
<Col key={metricName} span={12}>
|
||
<div className="chart-box">
|
||
<div className="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>
|
||
<div
|
||
className="expand-icon-box"
|
||
onClick={() => {
|
||
setChartDetail(data);
|
||
setShowChartDetailModal(true);
|
||
}}
|
||
>
|
||
<IconFont type="icon-chuangkoufangda" className="expand-icon" />
|
||
</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>
|
||
</Col>
|
||
);
|
||
})
|
||
) : chartLoading ? (
|
||
<></>
|
||
) : (
|
||
<Empty description="请先选择指标或刷新" style={{ width: '100%', height: '100%' }} />
|
||
)}
|
||
</Row>
|
||
</Spin>
|
||
</div>
|
||
</div>
|
||
{/* 历史配置变更记录内容 */}
|
||
<div className="config-change-records-container">{props.children}</div>
|
||
</div>
|
||
</div>
|
||
|
||
{/* 图表详情 */}
|
||
<Modal
|
||
width={1080}
|
||
visible={showChartDetailModal}
|
||
centered={true}
|
||
footer={null}
|
||
closable={false}
|
||
onCancel={() => setShowChartDetailModal(false)}
|
||
>
|
||
<div className="chart-detail-modal-container">
|
||
<div className="expand-icon-box" onClick={() => setShowChartDetailModal(false)}>
|
||
<IconFont type="icon-chuangkousuoxiao" className="expand-icon" />
|
||
</div>
|
||
{chartDetail && (
|
||
<SingleChart
|
||
chartTypeProp="line"
|
||
wrapStyle={{
|
||
width: 'auto',
|
||
height: 462,
|
||
}}
|
||
propChartData={chartDetail.metricLines}
|
||
{...getChartConfig({
|
||
metricName: `${chartDetail.metricName}{unit|(${chartDetail.metricUnit})}`,
|
||
})}
|
||
/>
|
||
)}
|
||
</div>
|
||
</Modal>
|
||
</div>
|
||
);
|
||
};
|
||
|
||
export default DetailChart;
|