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; 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([]); // 指标列表 const [selectedMetricNames, setSelectedMetricNames] = useState<(string | number)[]>([]); // 默认选中的指标的列表 const [metricDataList, setMetricDataList] = useState([]); const [messagesInMetricData, setMessagesInMetricData] = useState({ name: 'MessagesIn', unit: '', data: undefined, }); const [curHeaderOptions, setCurHeaderOptions] = useState(); const [defaultChartLoading, setDefaultChartLoading] = useState(true); const [chartLoading, setChartLoading] = useState(true); const metricRankList = useRef([]); 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 (
{ return record.name === DEFAULT_METRIC ? { disabled: true, } : {}; }, submitCallback: indicatorChangeCallback, }} />
{/* MessageIn 图表 */}
{messagesInMetricData.data && ( <>
{ let content = ''; const metricDefine = global.getMetricDefine(MetricType.Cluster, messagesInMetricData.name); if (metricDefine) { content = metricDefine.desc; } return content; }} > {messagesInMetricData.name} ({messagesInMetricData.unit})
{messagesInMetricData.data.length ? ( ) : ( !defaultChartLoading && )} )}
{/* 其余指标图表 */}
{metricDataList.length ? (
{metricDataList.map((data: any, i: number) => { const { metricName, metricUnit, metricLines } = data; return (
{ let content = ''; const metricDefine = global.getMetricDefine(MetricType.Cluster, metricName); if (metricDefine) { content = metricDefine.desc; } return content; }} > {metricName} ({metricUnit})
); })}
) : chartLoading ? ( <> ) : ( )}
{/* 历史配置变更记录内容 */}
{props.children}
); }; export default DetailChart;