Files
KnowStreaming/km-console/packages/layout-clusters-fe/src/pages/ConsumerGroup/ExpandedRow.tsx
erge 49d3d078d3 合并主分支 (#1199) (#1201)
请不要在没有先创建Issue的情况下创建Pull Request。

## 变更的目的是什么

XXXXX

## 简短的更新日志

- [Bugfix]修复重置offset接口调用过多问题
- [Bugfix]修复消费组Offset重置后,提示重置成功,但是前端不刷新数据,Offset无变化的问题
- [Optimize]消费组详情控制数据实时刷新

## 验证这一变化

XXXX

请遵循此清单,以帮助我们快速轻松地整合您的贡献:

* [ ] 一个 PR(Pull Request的简写)只解决一个问题,禁止一个 PR 解决多个问题;
* [ ] 确保 PR 有对应的 Issue(通常在您开始处理之前创建),除非是书写错误之类的琐碎更改不需要 Issue ;
* [ ] 格式化 PR 及 Commit-Log 的标题及内容,例如 #861 。PS:Commit-Log 需要在 Git Commit
代码时进行填写,在 GitHub 上修改不了;
* [ ] 编写足够详细的 PR 描述,以了解 PR 的作用、方式和原因;
* [ ] 编写必要的单元测试来验证您的逻辑更正。如果提交了新功能或重大更改,请记住在 test 模块中添加 integration-test;
* [ ] 确保编译通过,集成测试通过;

请不要在没有先创建Issue的情况下创建Pull Request。

## 变更的目的是什么

XXXXX

## 简短的更新日志

XX

## 验证这一变化

XXXX

请遵循此清单,以帮助我们快速轻松地整合您的贡献:

* [ ] 一个 PR(Pull Request的简写)只解决一个问题,禁止一个 PR 解决多个问题;
* [ ] 确保 PR 有对应的 Issue(通常在您开始处理之前创建),除非是书写错误之类的琐碎更改不需要 Issue ;
* [ ] 格式化 PR 及 Commit-Log 的标题及内容,例如 #861 。PS:Commit-Log 需要在 Git Commit
代码时进行填写,在 GitHub 上修改不了;
* [ ] 编写足够详细的 PR 描述,以了解 PR 的作用、方式和原因;
* [ ] 编写必要的单元测试来验证您的逻辑更正。如果提交了新功能或重大更改,请记住在 test 模块中添加 integration-test;
* [ ] 确保编译通过,集成测试通过;
2023-11-30 21:56:42 +08:00

413 lines
13 KiB
TypeScript

import React, { useEffect, useState } from 'react';
import { useParams, useHistory } from 'react-router-dom';
import { ProTable, Utils, DRangeTime, Select, SingleChart } from 'knowdesign';
import SwitchTab from '@src/components/SwitchTab';
import { CHART_COLOR_LIST, getBasicChartConfig } from '@src/constants/chartConfig';
import ContentWithCopy from '@src/components/CopyContent';
import { IconFont } from '@knowdesign/icons';
import API from '@src/api/index';
import { hashDataParse } from '@src/constants/common';
const { Option } = Select;
export interface MetricLine {
createTime?: number;
metricPoints: Array<{
aggType: string;
createTime: number;
timeStamp: number;
unit: string;
updateTime: number;
value: number;
}>;
name: string;
updateTime?: number;
}
export interface MetricData {
metricLines?: Array<MetricLine>;
metricLine?: MetricLine;
metricName: string;
}
export interface HashData {
groupName: string;
topicName: string;
}
const metricConsts = ['LogEndOffset', 'OffsetConsumed', 'Lag'];
const metricWithType = [
{ metricName: 'LogEndOffset', metricType: 104 },
{ metricName: 'OffsetConsumed', metricType: 102 },
{ metricName: 'Lag', metricType: 102 },
];
export const ExpandedRow: any = ({ record, groupName, refreshKey }: any) => {
const params: any = useParams<{
clusterId: string;
}>();
const history = useHistory();
const now = Date.now();
const [allGroupMetricsData, setAllGroupMetricsData] = useState<Array<MetricData>>([]);
const [showMode, setShowMode] = useState('table');
const [curPartition, setCurPartition] = useState<string>('');
const [partitionList, setPartitionList] = useState([]);
const [timeRange, setTimeRange] = useState([now - 24 * 60 * 60 * 1000, now]);
const [consumerListLoading, setConsumerListLoading] = useState(true);
const [consumerList, setConsumerList] = useState([]);
const [groupMetricsData, setGroupMetricsData] = useState<Array<MetricData>>([]);
const clusterId = Number(params.clusterId);
const [pagination, setPagination] = useState<any>({
current: 1,
pageSize: 5,
simple: true,
hideOnSinglePage: false,
});
const columns = [
{
title: 'Partition',
dataIndex: 'partitionId',
key: 'partitionId',
lineClampOne: true,
needTooltip: true,
width: 180,
},
{
title: 'Member ID',
dataIndex: 'memberId',
key: 'memberId',
width: 200,
render: (v: string) => {
return v ? <ContentWithCopy content={v} /> : '-';
},
},
{
title: 'Current Offset',
dataIndex: 'OffsetConsumed',
key: 'OffsetConsumed',
render: (v: any, record: any) => {
return record?.latestMetrics?.metrics?.OffsetConsumed.toLocaleString();
},
sorter: true,
// sorter: {
// compare: (a: any, b: any) => {
// let value1 = a?.metrics?.find((item: any) => item.metricName === 'OffsetConsumed' && item.metricType === 102)?.metricValue
// let value2 = b?.metrics?.find((item: any) => item.metricName === 'OffsetConsumed' && item.metricType === 102)?.metricValue
// return value1 - value2
// },
// multiple: 1
// }
},
{
title: 'Log End Offset',
dataIndex: 'LogEndOffset',
key: 'LogEndOffset',
render: (v: any, record: any) => {
return record?.latestMetrics?.metrics?.LogEndOffset.toLocaleString();
},
sorter: true,
// sorter: {
// compare: (a: any, b: any) => {
// let value1 = a?.metrics?.find((item: any) => item.metricName === 'LogEndOffset' && item.metricType === 104)?.metricValue
// let value2 = b?.metrics?.find((item: any) => item.metricName === 'LogEndOffset' && item.metricType === 104)?.metricValue
// return value1 - value2
// },
// multiple: 2
// }
},
{
title: 'Lag',
dataIndex: 'Lag',
key: 'Lag',
render: (v: any, record: any) => {
return record?.latestMetrics?.metrics?.Lag.toLocaleString();
},
sorter: true,
// sorter: {
// compare: (a: any, b: any) => {
// let value1 = a?.metrics?.find((item: any) => item.metricName === 'Lag' && item.metricType === 102)?.metricValue
// let value2 = b?.metrics?.find((item: any) => item.metricName === 'Lag' && item.metricType === 102)?.metricValue
// return value1 - value2
// },
// multiple: 3
// }
},
{
title: 'Host',
dataIndex: 'host',
key: 'host',
},
{
title: 'Client ID',
dataIndex: 'clientId',
key: 'clientId',
needTooltip: true,
lineClampOne: true,
width: 200,
},
];
const getTopicGroupMetric = ({ pagination = { current: 1, pageSize: 10 }, sorter = {} }: { pagination?: any; sorter?: any }) => {
setConsumerListLoading(true);
const params: any = {
// metricRealTimes: metricWithType,
latestMetricNames: metricConsts,
pageNo: pagination.current,
pageSize: pagination.pageSize,
sortField: sorter.field || undefined,
sortType: sorter.order ? sorter.order.substring(0, sorter.order.indexOf('end')) : undefined,
};
return Utils.post(
API.getTopicGroupMetric({
clusterId,
groupName: groupName,
topicName: record.topicName,
}),
params
)
.then((data: any) => {
if (!data) return;
setPagination({
current: data.pagination?.pageNo,
pageSize: data.pagination?.pageSize,
total: data.pagination?.total,
});
setConsumerList(data?.bizData || []);
})
.finally(() => {
setConsumerListLoading(false);
});
};
const getTopicGroupMetricHistory = (partitions: Array<any>) => {
const params = {
aggType: 'sum',
groupTopics: partitions.map((p: any) => ({
partition: p.value,
topic: record.topicName,
})),
group: groupName,
metricsNames: metricWithType.map((item) => item.metricName),
startTime: timeRange[0],
endTime: timeRange[1],
topNu: 0,
};
Utils.post(API.getTopicGroupMetricHistory(clusterId), params, { timeout: 300000 }).then((data: Array<MetricData>) => {
// ! 替换接口返回
setAllGroupMetricsData(data);
});
};
const getConsumersMetadata = (hashData: HashData) => {
return Utils.request(API.getConsumersMetadata(params.clusterId, groupName, record.topicName));
};
const getTopicsMetaData = () => {
return Utils.request(API.getTopicsMetaData(record?.topicName, +params.clusterId));
};
const onTableChange = (pagination: any, filters: any, sorter: any) => {
getTopicGroupMetric({ pagination, sorter });
};
useEffect(() => {
const hashData = hashDataParse(location.hash);
// if (!hashData.groupName) return;
// 获取分区列表 为图表模式做准备
getConsumersMetadata(hashData).then((res: any) => {
if (!res.exist) {
history.push(`/cluster/${params?.clusterId}/consumers`);
return;
}
getTopicsMetaData()
.then((data: any) => {
const partitionLists = (data?.partitionIdList || []).map((item: any) => {
return {
label: item,
value: item,
};
});
setCurPartition(partitionLists?.[0]?.value);
setPartitionList(partitionLists);
getTopicGroupMetricHistory(partitionLists);
})
.catch((e) => {
history.push(`/cluster/${params?.clusterId}/consumers`);
});
// 获取Consumer列表 表格模式
getTopicGroupMetric({});
});
}, [hashDataParse(location.hash).groupName, refreshKey]);
useEffect(() => {
if (partitionList.length === 0) return;
getTopicGroupMetricHistory(partitionList);
}, [timeRange]);
useEffect(() => {
if (curPartition === '' || allGroupMetricsData.length === 0) return;
const filteredData = allGroupMetricsData.map((item) => {
const allData = item.metricLines.reduce(
(acc, cur) => {
if (acc.metricLine.metricPoints.length === 0) {
acc.metricLine.metricPoints = cur.metricPoints.map((p: any) => ({
timeStamp: p.timeStamp,
value: Number(p.value),
}));
} else {
acc.metricLine.metricPoints.forEach((mp) => {
const curMetricPoint = cur.metricPoints.find((curmp) => curmp.timeStamp === mp.timeStamp);
mp.value += curMetricPoint ? Number(curMetricPoint.value) : 0;
});
}
return acc;
},
{
metricName: item.metricName,
metricLine: {
name: 'all',
metricPoints: [],
},
}
);
return curPartition === '__all__'
? allData
: {
metricName: item.metricName,
metricLine: item.metricLines.find((line) => line.name.indexOf(curPartition) >= 0),
};
});
setGroupMetricsData(filteredData);
}, [curPartition, allGroupMetricsData]);
return (
<div
key={record.key}
style={{ position: 'relative', padding: '12px 16px', border: '1px solid #EFF2F7', borderRadius: '8px', backgroundColor: '#ffffff' }}
>
<div className="consumer-group-detail">
<div className="title-and-mode" style={{ height: '30px', paddingBottom: '12px' }}>
<div className="title-and-mode-header"></div>
<div className="right">
{showMode === 'chart' && (
<Select
style={{ width: 140, marginRight: 8 }}
size="small"
value={curPartition}
onChange={(id) => {
setCurPartition(id);
}}
>
<Option value={'__all__'}>Partition</Option>
{partitionList.map((partition) => (
<Option key={partition.value} value={partition.value}>
{partition.value}
</Option>
))}
</Select>
)}
{showMode === 'chart' && (
<DRangeTime
rangeTimeArr={timeRange}
timeChange={(o: any) => {
setTimeRange(o);
}}
></DRangeTime>
)}
{showMode === 'chart' && <div className="divider"></div>}
<SwitchTab defaultKey={showMode} onChange={(key) => setShowMode(key)}>
<SwitchTab.TabItem key="chart">
<div style={{ width: 34, height: 23 }}>
<IconFont type="icon-tubiao"></IconFont>
</div>
</SwitchTab.TabItem>
<SwitchTab.TabItem key="table">
<div style={{ width: 34, height: 23 }}>
<IconFont type="icon-biaoge"></IconFont>
</div>
</SwitchTab.TabItem>
</SwitchTab>
</div>
</div>
{showMode === 'table' && (
<ProTable
showQueryForm={false}
tableProps={{
isCustomPg: false,
loading: consumerListLoading,
showHeader: false,
rowKey: 'partitionId',
columns: columns,
dataSource: consumerList || [],
paginationProps: { ...pagination },
attrs: {
sortDirections: ['descend', 'ascend', 'default'],
scroll: { x: 1000 },
size: 'small',
bordered: false,
className: 'expanded-table',
rowClassName: 'table-small-bgcolor',
// className: 'frameless-table', // 纯无边框表格类名
onChange: onTableChange,
},
}}
/>
)}
{showMode === 'chart' && (
<div className="single-chart">
<SingleChart
showHeader={false}
wrapStyle={{
width: '100%',
height: 242,
}}
option={getBasicChartConfig({
xAxis: {
type: 'time',
},
title: {
show: false,
},
legend: {
left: 'center',
},
color: CHART_COLOR_LIST,
grid: {
left: 0,
right: 0,
top: 10,
},
tooltip: {
customWidth: 200,
},
})}
chartTypeProp="line"
connectEventName=""
propChartData={groupMetricsData}
seriesCallback={(data: any) => {
return data.map((metricData: any) => {
const partitionMetricData = metricData.metricLine?.metricPoints || [];
return {
name: metricData.metricName,
data: partitionMetricData.map((item: any) => [item.timeStamp, item.value, item.unit]),
lineStyle: {
width: 1.5,
},
smooth: 0.25,
symbol: 'emptyCircle',
symbolSize: 4,
emphasis: {
disabled: true,
},
};
});
}}
/>
</div>
)}
</div>
</div>
);
};