mirror of
https://github.com/didi/KnowStreaming.git
synced 2025-12-24 11:52:08 +08:00
fix: ConsumerGroup 列表 & 详情页重构
This commit is contained in:
@@ -0,0 +1,249 @@
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import { useParams, useHistory } from 'react-router-dom';
|
||||
import { Drawer, ProTable, Utils } from 'knowdesign';
|
||||
import { IconFont } from '@knowdesign/icons';
|
||||
import API from '@src/api/index';
|
||||
import { defaultPagination, hashDataParse } from '@src/constants/common';
|
||||
import { getGtoupTopicColumns } from './config';
|
||||
import { ExpandedRow } from './ExpandedRow';
|
||||
import ResetOffsetDrawer from './ResetOffsetDrawer';
|
||||
const { request } = Utils;
|
||||
|
||||
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 },
|
||||
];
|
||||
|
||||
const GroupDetail = (props: any) => {
|
||||
const { scene } = props;
|
||||
const urlParams = useParams<{
|
||||
clusterId: string;
|
||||
}>();
|
||||
const now = Date.now();
|
||||
const history = useHistory();
|
||||
const [hashData, setHashData] = useState<HashData>({ groupName: '', topicName: '' });
|
||||
const [visible, setVisible] = useState(false);
|
||||
|
||||
const [topicData, setTopicData] = useState([]);
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [pagination, setPagination] = useState<any>(defaultPagination);
|
||||
const [expandedData, setExpandedData] = useState([]);
|
||||
const [chartData, setChartData] = useState<Array<MetricData>>([]);
|
||||
const [loadingObj, setLoadingObj] = useState<any>({});
|
||||
const [timeRange, setTimeRange] = useState([now - 24 * 60 * 60 * 1000, now]);
|
||||
const [curPartition, setCurPartition] = useState<string>('');
|
||||
const [groupMetricsData, setGroupMetricsData] = useState<Array<MetricData>>([]);
|
||||
const [openKeys, setOpenKeys] = useState();
|
||||
const [resetOffsetVisible, setResetOffsetVisible] = useState(false);
|
||||
const [resetOffsetArg, setResetOffsetArg] = useState({});
|
||||
|
||||
const genData = async ({ pageNo, pageSize, groupName }: any) => {
|
||||
if (urlParams?.clusterId === undefined) return;
|
||||
|
||||
setLoading(true);
|
||||
const params = {
|
||||
// searchKeywords: '',
|
||||
pageNo,
|
||||
pageSize,
|
||||
};
|
||||
|
||||
request(API.getGroupTopicList(+urlParams?.clusterId, groupName), { params })
|
||||
.then((res: any) => {
|
||||
setVisible(true);
|
||||
setPagination({
|
||||
current: res.pagination?.pageNo,
|
||||
pageSize: res.pagination?.pageSize,
|
||||
total: res.pagination?.total,
|
||||
});
|
||||
const newTopicListData = res?.bizData.map((item: any) => {
|
||||
return {
|
||||
...item,
|
||||
key: item.topicName,
|
||||
};
|
||||
});
|
||||
setTopicData(newTopicListData || []);
|
||||
setLoading(false);
|
||||
})
|
||||
.catch((err) => {
|
||||
setLoading(false);
|
||||
});
|
||||
};
|
||||
|
||||
const onClose = () => {
|
||||
setVisible(false);
|
||||
// clean hash'
|
||||
scene === 'topicDetail' && history.goBack();
|
||||
scene !== 'topicDetail' && window.history.pushState('', '', location.pathname);
|
||||
};
|
||||
|
||||
const resetOffset = (record: any) => {
|
||||
setResetOffsetVisible(true);
|
||||
setResetOffsetArg({
|
||||
topicName: record?.topicName,
|
||||
groupName: record?.groupName,
|
||||
});
|
||||
};
|
||||
|
||||
const onTableChange = (pagination: any, filters: any, sorter: any) => {
|
||||
genData({ pageNo: pagination.current, pageSize: pagination.pageSize, filters, sorter, groupName: hashData.groupName });
|
||||
};
|
||||
|
||||
const onClickExpand = (expanded: any, record: any) => {
|
||||
const key = record?.key;
|
||||
// 之前展开过
|
||||
if (expandedData[key]?.length) return;
|
||||
// 第一次展开
|
||||
setOpenKeys(key);
|
||||
// const loading = { ...loadingObj };
|
||||
// loading[key] = true;
|
||||
// setLoadingObj(loading);
|
||||
// expanded && queryExpandedData(record, key);
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
const hashData = hashDataParse(location.hash);
|
||||
if (!hashData.groupName) {
|
||||
setVisible(false);
|
||||
}
|
||||
setHashData(hashData);
|
||||
// 获取分区列表 为图表模式做准备
|
||||
hashData.groupName && genData({ pageNo: 1, pageSize: pagination.pageSize, groupName: hashData.groupName });
|
||||
// getConsumersMetadata(hashData).then((res: any) => {
|
||||
// if (res.exist) {
|
||||
// setVisible(false);
|
||||
// history.push(`/cluster/${params?.clusterId}/consumers`);
|
||||
// return;
|
||||
// }
|
||||
// setVisible(true);
|
||||
// getTopicGroupPartitionsHistory(hashData)
|
||||
// .then((data: any) => {
|
||||
// if (data.length > 0) {
|
||||
// setCurPartition(data[0].partition);
|
||||
// }
|
||||
// setPartitionList(data);
|
||||
// return data;
|
||||
// })
|
||||
// .then((data) => {
|
||||
// getTopicGroupMetricHistory(data, hashData);
|
||||
// })
|
||||
// .catch((e) => {
|
||||
// history.push(`/cluster/${params?.clusterId}/consumers`);
|
||||
// setVisible(false);
|
||||
// });
|
||||
// // 获取Consumer列表 表格模式
|
||||
// getTopicGroupMetric(hashData);
|
||||
// });
|
||||
}, [hashDataParse(location.hash).groupName]);
|
||||
|
||||
return (
|
||||
<Drawer
|
||||
push={false}
|
||||
title="Consumer Group详情"
|
||||
width={1080}
|
||||
placement="right"
|
||||
onClose={onClose}
|
||||
visible={visible}
|
||||
className="consumer-group-detail-drawer"
|
||||
maskClosable={false}
|
||||
destroyOnClose
|
||||
// extra={
|
||||
// <Space>
|
||||
// {global.hasPermission &&
|
||||
// global.hasPermission(
|
||||
// scene === 'topicDetail' ? ClustersPermissionMap.TOPIC_RESET_OFFSET : ClustersPermissionMap.CONSUMERS_RESET_OFFSET
|
||||
// ) && <ResetOffsetDrawer record={hashData}></ResetOffsetDrawer>}
|
||||
// <Divider type="vertical" />
|
||||
// </Space>
|
||||
// }
|
||||
>
|
||||
<ProTable
|
||||
showQueryForm={false}
|
||||
tableProps={{
|
||||
showHeader: false,
|
||||
rowKey: 'key',
|
||||
loading: loading,
|
||||
columns: getGtoupTopicColumns({ resetOffset }),
|
||||
dataSource: topicData,
|
||||
paginationProps: { ...pagination },
|
||||
// noPagination: true,
|
||||
attrs: {
|
||||
className: 'consumer-group-detail-drawer-table',
|
||||
bordered: false,
|
||||
onChange: onTableChange,
|
||||
tableLayout: 'auto',
|
||||
scroll: { x: 'max-content' },
|
||||
expandable: {
|
||||
expandedRowRender: (record: any, index: number, indent: any, expanded: boolean) => (
|
||||
<ExpandedRow
|
||||
record={record}
|
||||
openKeys={openKeys}
|
||||
expanded={expanded}
|
||||
tableData={expandedData}
|
||||
chartData={chartData}
|
||||
groupName={hashDataParse(location.hash).groupName}
|
||||
loading={loadingObj}
|
||||
/>
|
||||
),
|
||||
// expandedRowRender,
|
||||
onExpand: onClickExpand,
|
||||
columnWidth: '20px',
|
||||
fixed: 'left',
|
||||
expandIcon: ({ expanded, onExpand, record }: any) => {
|
||||
return expanded ? (
|
||||
<IconFont
|
||||
style={{ fontSize: '16px' }}
|
||||
type="icon-xia"
|
||||
onClick={(e: any) => {
|
||||
onExpand(record, e);
|
||||
}}
|
||||
/>
|
||||
) : (
|
||||
<IconFont
|
||||
style={{ fontSize: '16px' }}
|
||||
type="icon-jiantou_1"
|
||||
onClick={(e: any) => {
|
||||
onExpand(record, e);
|
||||
}}
|
||||
/>
|
||||
);
|
||||
},
|
||||
},
|
||||
style: {
|
||||
width: '1032px',
|
||||
},
|
||||
},
|
||||
}}
|
||||
/>
|
||||
<ResetOffsetDrawer visible={resetOffsetVisible} setVisible={setResetOffsetVisible} record={resetOffsetArg}></ResetOffsetDrawer>
|
||||
</Drawer>
|
||||
);
|
||||
};
|
||||
|
||||
export default GroupDetail;
|
||||
@@ -0,0 +1,413 @@
|
||||
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 }: 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((origin: any) => {
|
||||
return { ...origin, current: data.pagination?.pageNo, pageSize: data.pagination?.pageSize };
|
||||
});
|
||||
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).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(() => {
|
||||
// getTopicGroupMetric();
|
||||
// }, [sortObj]);
|
||||
|
||||
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]);
|
||||
|
||||
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>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,187 @@
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import { Button, DatePicker, Drawer, Form, notification, Radio, Utils, Space, Divider, message } from 'knowdesign';
|
||||
import { useParams } from 'react-router-dom';
|
||||
import EditTable from '../TestingProduce/component/EditTable';
|
||||
import Api from '@src/api/index';
|
||||
import moment from 'moment';
|
||||
|
||||
const CustomSelectResetTime = (props: { value?: string; onChange?: (val: Number | String) => void }) => {
|
||||
const { value, onChange } = props;
|
||||
const [timeSetMode, setTimeSetMode] = useState('newest');
|
||||
useEffect(() => {
|
||||
onChange('newest');
|
||||
}, []);
|
||||
return (
|
||||
<>
|
||||
<Radio.Group
|
||||
style={{
|
||||
marginBottom: 20,
|
||||
}}
|
||||
onChange={(e) => {
|
||||
setTimeSetMode(e.target.value);
|
||||
if (e.target.value === 'newest') {
|
||||
onChange('newest');
|
||||
}
|
||||
}}
|
||||
value={timeSetMode}
|
||||
>
|
||||
<Radio value={'newest'}>最新Offset</Radio>
|
||||
<Radio value={'custom'}>自定义</Radio>
|
||||
</Radio.Group>
|
||||
{timeSetMode === 'custom' && (
|
||||
<DatePicker
|
||||
value={moment(value === 'newest' ? Date.now() : value)}
|
||||
style={{ width: '100%' }}
|
||||
showTime={true}
|
||||
onChange={(v) => {
|
||||
onChange(v.valueOf());
|
||||
}}
|
||||
></DatePicker>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default (props: any) => {
|
||||
const { record, visible, setVisible } = props;
|
||||
const routeParams = useParams<{
|
||||
clusterId: string;
|
||||
}>();
|
||||
const [form] = Form.useForm();
|
||||
const defaultResetType = 'assignedTime';
|
||||
const [resetType, setResetType] = useState(defaultResetType);
|
||||
const customFormRef: any = React.createRef();
|
||||
const clusterPhyId = Number(routeParams.clusterId);
|
||||
const [partitionIdList, setPartitionIdList] = useState([]);
|
||||
useEffect(() => {
|
||||
form.setFieldsValue({
|
||||
resetType: defaultResetType,
|
||||
});
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
visible &&
|
||||
Utils.request(Api.getTopicsMetaData(record?.topicName, +routeParams.clusterId))
|
||||
.then((res: any) => {
|
||||
const partitionLists = (res?.partitionIdList || []).map((item: any) => {
|
||||
return {
|
||||
label: item,
|
||||
value: item,
|
||||
};
|
||||
});
|
||||
setPartitionIdList(partitionLists);
|
||||
})
|
||||
.catch((err) => {
|
||||
message.error(err);
|
||||
});
|
||||
}, [visible]);
|
||||
const confirm = () => {
|
||||
let tableData;
|
||||
if (customFormRef.current) {
|
||||
tableData = customFormRef.current.getTableData();
|
||||
}
|
||||
const formData = form.getFieldsValue();
|
||||
let resetParams: any = {
|
||||
clusterId: clusterPhyId,
|
||||
createIfNotExist: false,
|
||||
groupName: record.groupName,
|
||||
topicName: record.topicName,
|
||||
};
|
||||
if (formData.resetType === 'assignedTime') {
|
||||
resetParams.resetType = formData.timestamp === 'newest' ? 0 : 2;
|
||||
if (resetParams.resetType === 2) {
|
||||
resetParams.timestamp = formData.timestamp;
|
||||
}
|
||||
}
|
||||
if (formData.resetType === 'partition') {
|
||||
resetParams.resetType = 3;
|
||||
resetParams.offsetList = tableData
|
||||
? tableData.map((item: { key: string; value: string }) => ({ partitionId: item.key, offset: item.value }))
|
||||
: [];
|
||||
}
|
||||
Utils.put(Api.resetGroupOffset(), resetParams).then((data) => {
|
||||
if (data === null) {
|
||||
notification.success({
|
||||
message: '重置offset成功',
|
||||
});
|
||||
setVisible(false);
|
||||
} else {
|
||||
notification.error({
|
||||
message: '重置offset失败',
|
||||
});
|
||||
setVisible(false);
|
||||
}
|
||||
});
|
||||
};
|
||||
return (
|
||||
<>
|
||||
<Drawer
|
||||
title="重置Offset"
|
||||
width={480}
|
||||
visible={visible}
|
||||
maskClosable={false}
|
||||
extra={
|
||||
<Space>
|
||||
<Button
|
||||
size="small"
|
||||
style={{ marginRight: 8 }}
|
||||
onClick={(_) => {
|
||||
setVisible(false);
|
||||
}}
|
||||
>
|
||||
取消
|
||||
</Button>
|
||||
<Button size="small" type="primary" onClick={confirm}>
|
||||
确定
|
||||
</Button>
|
||||
<Divider type="vertical" />
|
||||
</Space>
|
||||
}
|
||||
className="cluster-detail-consumer-resetoffset"
|
||||
onClose={(_) => {
|
||||
setVisible(false);
|
||||
}}
|
||||
>
|
||||
<Form form={form} labelCol={{ span: 5 }} layout="vertical" className="reset-offset-form">
|
||||
<Form.Item name="resetType" label="重置类型" required>
|
||||
<Radio.Group
|
||||
defaultValue="assignedTime"
|
||||
value={resetType}
|
||||
onChange={(e) => {
|
||||
setResetType(e.target.value);
|
||||
}}
|
||||
>
|
||||
<Radio value={'assignedTime'}>重置到指定时间</Radio>
|
||||
<Radio value={'partition'}>重置分区</Radio>
|
||||
</Radio.Group>
|
||||
</Form.Item>
|
||||
{resetType === 'assignedTime' && (
|
||||
<Form.Item name="timestamp" label="时间" required>
|
||||
<CustomSelectResetTime />
|
||||
</Form.Item>
|
||||
)}
|
||||
{resetType === 'partition' && (
|
||||
<Form.Item name="partition" label="分区及偏移" required>
|
||||
<EditTable
|
||||
ref={customFormRef}
|
||||
colCustomConfigs={[
|
||||
{
|
||||
title: 'PartitionID',
|
||||
inputType: 'select',
|
||||
placeholder: '请输入Partition',
|
||||
options: partitionIdList,
|
||||
},
|
||||
{
|
||||
title: 'Offset',
|
||||
inputType: 'number',
|
||||
placeholder: '请输入Offset',
|
||||
},
|
||||
]}
|
||||
></EditTable>
|
||||
</Form.Item>
|
||||
)}
|
||||
</Form>
|
||||
</Drawer>
|
||||
</>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,117 @@
|
||||
/* eslint-disable @typescript-eslint/explicit-module-boundary-types */
|
||||
import React from 'react';
|
||||
import { AppContainer } from 'knowdesign';
|
||||
import TagsWithHide from '@src/components/TagsWithHide';
|
||||
import { ClustersPermissionMap } from '../CommonConfig';
|
||||
|
||||
export const runningStatusEnum: any = {
|
||||
1: 'Doing',
|
||||
2: 'Prepare',
|
||||
3: 'Success',
|
||||
4: 'Failed',
|
||||
5: 'Canceled',
|
||||
};
|
||||
|
||||
export const defaultPagination = {
|
||||
current: 1,
|
||||
pageSize: 10,
|
||||
position: 'bottomRight',
|
||||
showSizeChanger: true,
|
||||
pageSizeOptions: ['10', '20', '50', '100', '200', '500'],
|
||||
};
|
||||
|
||||
export const getGroupColumns = (arg?: any) => {
|
||||
const columns = [
|
||||
{
|
||||
title: 'ConsumerGroup',
|
||||
dataIndex: 'name',
|
||||
key: 'name',
|
||||
lineClampTwo: true,
|
||||
render: (v: any, r: any) => {
|
||||
return (
|
||||
<a
|
||||
onClick={() => {
|
||||
window.location.hash = `groupName=${v || ''}`;
|
||||
}}
|
||||
>
|
||||
{v}
|
||||
</a>
|
||||
);
|
||||
},
|
||||
width: 200,
|
||||
},
|
||||
{
|
||||
title: '消费的Topic',
|
||||
dataIndex: 'topicNameList',
|
||||
key: 'topicNameList',
|
||||
width: 200,
|
||||
render(t: any, r: any) {
|
||||
return t && t.length > 0 ? <TagsWithHide placement="bottom" list={t} expandTagContent={(num: any) => `共有${num}个`} /> : '-';
|
||||
},
|
||||
},
|
||||
{
|
||||
title: 'Status',
|
||||
dataIndex: 'state',
|
||||
key: 'state',
|
||||
width: 200,
|
||||
},
|
||||
{
|
||||
title: 'Member数',
|
||||
dataIndex: 'memberCount',
|
||||
key: 'memberCount',
|
||||
width: 200,
|
||||
render: (t: number) => (t ? t.toLocaleString() : '-'),
|
||||
},
|
||||
];
|
||||
return columns;
|
||||
};
|
||||
|
||||
export const getGtoupTopicColumns = (arg?: any) => {
|
||||
const [global] = AppContainer.useGlobalValue();
|
||||
const columns: any = [
|
||||
{
|
||||
title: 'Topic名称',
|
||||
dataIndex: 'topicName',
|
||||
key: 'topicName',
|
||||
needTooltip: true,
|
||||
lineClampOne: true,
|
||||
width: 150,
|
||||
},
|
||||
{
|
||||
title: 'Status',
|
||||
dataIndex: 'state',
|
||||
key: 'state',
|
||||
width: 150,
|
||||
},
|
||||
{
|
||||
title: 'Max Lag',
|
||||
dataIndex: 'maxLag',
|
||||
key: 'maxLag',
|
||||
width: 150,
|
||||
render: (t: number) => (t ? t.toLocaleString() : '-'),
|
||||
},
|
||||
{
|
||||
title: 'Member数',
|
||||
dataIndex: 'memberCount',
|
||||
key: 'memberCount',
|
||||
width: 150,
|
||||
render: (t: number) => (t ? t.toLocaleString() : '-'),
|
||||
},
|
||||
];
|
||||
if (global.hasPermission && global.hasPermission(ClustersPermissionMap.CONSUMERS_RESET_OFFSET)) {
|
||||
columns.push({
|
||||
title: '操作',
|
||||
dataIndex: 'desc',
|
||||
key: 'desc',
|
||||
width: 150,
|
||||
render: (value: any, record: any) => {
|
||||
return (
|
||||
<div>
|
||||
<a onClick={() => arg.resetOffset(record)}>重置Offset</a>
|
||||
</div>
|
||||
);
|
||||
},
|
||||
});
|
||||
}
|
||||
return columns;
|
||||
};
|
||||
@@ -0,0 +1,186 @@
|
||||
.brokerList {
|
||||
.d-table-box-header {
|
||||
padding: 0 0 12px 0;
|
||||
}
|
||||
.tag-success,
|
||||
.tag-error {
|
||||
padding: 2px 8px;
|
||||
border-radius: 4px;
|
||||
margin-left: 4px;
|
||||
}
|
||||
.tag-success {
|
||||
background: #f1faf2;
|
||||
color: #73d380;
|
||||
}
|
||||
.tag-error {
|
||||
background: #fff0ef;
|
||||
color: #ff7066;
|
||||
}
|
||||
}
|
||||
|
||||
.operating-state {
|
||||
.consumers-search {
|
||||
display: contents;
|
||||
.search-input-short {
|
||||
margin-right: 8px;
|
||||
}
|
||||
}
|
||||
.pro-table-wrap {
|
||||
// padding: 17px 24px;
|
||||
background: #fff;
|
||||
border-radius: 12px;
|
||||
}
|
||||
}
|
||||
.consumer-group-detail-drawer {
|
||||
&-table {
|
||||
.dcloud-table-cell {
|
||||
padding: 7px 16px 8px 2px !important;
|
||||
}
|
||||
.dcloud-table-row-expand-icon-cell {
|
||||
padding: 7px 7px 5px 24px !important;
|
||||
}
|
||||
|
||||
.dcloud-table-expanded-row-fixed {
|
||||
padding: 16px 20px !important;
|
||||
.dcloud-table-cell {
|
||||
font-size: 12px !important;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.dcloud-drawer-extra {
|
||||
button {
|
||||
width: 90px;
|
||||
}
|
||||
.divider {
|
||||
width: 1px;
|
||||
height: 17px;
|
||||
margin: 0 16px;
|
||||
background: rgba(206, 212, 218, 0.6);
|
||||
}
|
||||
}
|
||||
.dcloud-drawer-body {
|
||||
padding-top: 2px !important;
|
||||
}
|
||||
}
|
||||
|
||||
.consumer-group-detail {
|
||||
// border: 1px solid #EFF2F7;
|
||||
// border-radius: 8px;
|
||||
// padding: 12px 16px;
|
||||
.title-and-mode {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
height: 60px;
|
||||
&-header {
|
||||
font-family: @font-family-bold;
|
||||
font-size: 13px;
|
||||
}
|
||||
h4 {
|
||||
font-family: PingFangSC-Semibold;
|
||||
}
|
||||
.right {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
.d-range-time-input {
|
||||
height: 27px !important;
|
||||
padding: 0 11px;
|
||||
input {
|
||||
line-height: 100%;
|
||||
}
|
||||
}
|
||||
.divider {
|
||||
width: 1px;
|
||||
height: 20px;
|
||||
background: rgba(206, 212, 218, 0.6);
|
||||
margin: 0 8px;
|
||||
}
|
||||
.switch-mode {
|
||||
.dcloud-radio-button-wrapper {
|
||||
font-size: 14px;
|
||||
width: 34px;
|
||||
height: 23px;
|
||||
padding: 0;
|
||||
line-height: 22px;
|
||||
text-align: center;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
.single-chart,
|
||||
.table {
|
||||
background: #f8f9fa;
|
||||
// margin-top: 12px;
|
||||
border-radius: 8px;
|
||||
.dcloud-table {
|
||||
height: 210px;
|
||||
overflow: auto;
|
||||
background-color: transparent;
|
||||
.dcloud-table-content .dcloud-table-cell {
|
||||
background-color: transparent;
|
||||
}
|
||||
}
|
||||
.dcloud-pagination {
|
||||
height: 32px;
|
||||
margin-bottom: 0;
|
||||
margin-top: 8px;
|
||||
}
|
||||
}
|
||||
.single-chart {
|
||||
padding: 16px 22px 4px;
|
||||
.single-chart-header {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
.content-with-copy {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
.content {
|
||||
flex: 1;
|
||||
display: inline-block;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
word-break: break-all;
|
||||
}
|
||||
.copy-icon {
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
padding-top: 2px;
|
||||
border-radius: 50%;
|
||||
margin-left: 4px;
|
||||
font-size: 16px;
|
||||
color: #adb5bc;
|
||||
opacity: 0;
|
||||
&:hover {
|
||||
background: rgba(33, 37, 41, 0.04);
|
||||
color: #74788d;
|
||||
}
|
||||
}
|
||||
}
|
||||
.dcloud-table-cell-row-hover {
|
||||
.copy-icon {
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
.cluster-detail-consumer-resetoffset {
|
||||
.reset-offset-form {
|
||||
.dcloud-radio-wrapper {
|
||||
width: 154px;
|
||||
}
|
||||
}
|
||||
.operate-wrap {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
.operate-btns-divider {
|
||||
width: 1px;
|
||||
height: 17px;
|
||||
background: rgba(0, 0, 0, 0.13);
|
||||
margin: 0 16px;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,134 @@
|
||||
import React, { useState, useEffect, memo } from 'react';
|
||||
import { useParams } from 'react-router-dom';
|
||||
import { ProTable, Utils, AppContainer, SearchInput, Divider, Form, Input, Button } from 'knowdesign';
|
||||
import { IconFont } from '@knowdesign/icons';
|
||||
import API from '../../api';
|
||||
import { getGroupColumns, defaultPagination } from './config';
|
||||
import { tableHeaderPrefix } from '@src/constants/common';
|
||||
import BrokerDetail from '../BrokerDetail';
|
||||
import DBreadcrumb from 'knowdesign/es/extend/d-breadcrumb';
|
||||
import ConsumerGroupHealthCheck from '@src/components/CardBar/ConsumerGroupHealthCheck';
|
||||
import GroupDetail from './Detail';
|
||||
import './index.less';
|
||||
|
||||
const { request } = Utils;
|
||||
|
||||
const BrokerList: React.FC = (props: any) => {
|
||||
const [global] = AppContainer.useGlobalValue();
|
||||
const [form] = Form.useForm();
|
||||
const urlParams = useParams<any>(); // 获取地址栏参数
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [data, setData] = useState([]);
|
||||
const [searchKeywords, setSearchKeywords] = useState('');
|
||||
const [pagination, setPagination] = useState<any>(defaultPagination);
|
||||
const [searchWords, setSearchWords] = useState<{ groupName: string; topicName: string }>({ groupName: '', topicName: '' });
|
||||
// 请求接口获取数据
|
||||
const genData = async ({ pageNo, pageSize }: any) => {
|
||||
if (urlParams?.clusterId === undefined) return;
|
||||
|
||||
setLoading(true);
|
||||
const params = {
|
||||
searchGroupName: searchWords.groupName ? searchWords.groupName.slice(0, 128) : undefined,
|
||||
searchTopicName: searchWords.topicName ? searchWords.topicName.slice(0, 128) : undefined,
|
||||
pageNo,
|
||||
pageSize,
|
||||
};
|
||||
|
||||
request(API.getOperatingStateList(urlParams?.clusterId), { params })
|
||||
.then((res: any) => {
|
||||
setPagination({
|
||||
current: res.pagination?.pageNo,
|
||||
pageSize: res.pagination?.pageSize,
|
||||
total: res.pagination?.total,
|
||||
});
|
||||
setData(res?.bizData || []);
|
||||
setLoading(false);
|
||||
})
|
||||
.catch((err) => {
|
||||
setLoading(false);
|
||||
});
|
||||
};
|
||||
|
||||
// 查询
|
||||
const onFinish = (formData: any) => {
|
||||
setSearchWords(formData);
|
||||
};
|
||||
|
||||
const onTableChange = (pagination: any, filters: any, sorter: any) => {
|
||||
genData({ pageNo: pagination.current, pageSize: pagination.pageSize, filters, sorter });
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
genData({
|
||||
pageNo: 1,
|
||||
pageSize: pagination.pageSize,
|
||||
});
|
||||
}, [searchWords]);
|
||||
|
||||
return (
|
||||
<div key="groupList" className="groupList">
|
||||
<div className="breadcrumb" style={{ marginBottom: '10px' }}>
|
||||
<DBreadcrumb
|
||||
breadcrumbs={[
|
||||
{ label: '多集群管理', aHref: '/' },
|
||||
{ label: global?.clusterInfo?.name, aHref: `/cluster/${global?.clusterInfo?.id}` },
|
||||
{ label: 'Consumers', aHref: `` },
|
||||
]}
|
||||
/>
|
||||
</div>
|
||||
<div style={{ margin: '12px 0' }}>
|
||||
<ConsumerGroupHealthCheck />
|
||||
</div>
|
||||
<div className="clustom-table-content">
|
||||
<div className={tableHeaderPrefix}>
|
||||
<div className={`${tableHeaderPrefix}-left`}>
|
||||
<div
|
||||
className={`${tableHeaderPrefix}-left-refresh`}
|
||||
onClick={() => genData({ pageNo: pagination.current, pageSize: pagination.pageSize })}
|
||||
>
|
||||
<IconFont className={`${tableHeaderPrefix}-left-refresh-icon`} type="icon-shuaxin1" />
|
||||
</div>
|
||||
<Divider type="vertical" className={`${tableHeaderPrefix}-divider`} />
|
||||
<Form form={form} layout="inline">
|
||||
<Form.Item name="groupName">
|
||||
<Input allowClear style={{ width: '190px' }} placeholder="请输入Consumer Group" />
|
||||
</Form.Item>
|
||||
<Form.Item name="topicName">
|
||||
<Input allowClear style={{ width: '190px' }} placeholder="请输入Topic name" />
|
||||
</Form.Item>
|
||||
</Form>
|
||||
<div>
|
||||
<Form style={{ justifyContent: 'flex-end' }} form={form} layout="inline" onFinish={onFinish}>
|
||||
<Form.Item style={{ marginRight: 0 }}>
|
||||
<Button type="primary" ghost htmlType="submit">
|
||||
查询
|
||||
</Button>
|
||||
</Form.Item>
|
||||
</Form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<ProTable
|
||||
key="groupTable"
|
||||
showQueryForm={false}
|
||||
tableProps={{
|
||||
showHeader: false,
|
||||
rowKey: 'group_list',
|
||||
loading: loading,
|
||||
columns: getGroupColumns(),
|
||||
dataSource: data,
|
||||
paginationProps: { ...pagination },
|
||||
attrs: {
|
||||
onChange: onTableChange,
|
||||
scroll: { y: 'calc(100vh - 400px)' },
|
||||
bordered: false,
|
||||
},
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
{<GroupDetail />}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default BrokerList;
|
||||
@@ -0,0 +1,502 @@
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import { useParams, useHistory } from 'react-router-dom';
|
||||
import { AppContainer, Divider, Drawer, ProTable, Select, SingleChart, Space, Tooltip, Utils } from 'knowdesign';
|
||||
import { IconFont } from '@knowdesign/icons';
|
||||
import { DRangeTime } from 'knowdesign';
|
||||
import { CHART_COLOR_LIST, getBasicChartConfig } from '@src/constants/chartConfig';
|
||||
import Api from '@src/api/index';
|
||||
import { hashDataParse } from '@src/constants/common';
|
||||
import { ClustersPermissionMap } from '../CommonConfig';
|
||||
import ResetOffsetDrawer from './ResetOffsetDrawer';
|
||||
import SwitchTab from '@src/components/SwitchTab';
|
||||
import ContentWithCopy from '@src/components/CopyContent';
|
||||
|
||||
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 default (props: any) => {
|
||||
const { scene, visible, setVisible, hashData } = props;
|
||||
const params = useParams<{
|
||||
clusterId: string;
|
||||
}>();
|
||||
const history = useHistory();
|
||||
const [global] = AppContainer.useGlobalValue();
|
||||
// const { record } = props;
|
||||
const now = Date.now();
|
||||
const [allGroupMetricsData, setAllGroupMetricsData] = useState<Array<MetricData>>([]);
|
||||
const [groupMetricsData, setGroupMetricsData] = useState<Array<MetricData>>([]);
|
||||
const [timeRange, setTimeRange] = useState([now - 24 * 60 * 60 * 1000, now]);
|
||||
const [consumerList, setConsumerList] = useState([]);
|
||||
const [partitionList, setPartitionList] = useState([]);
|
||||
const [curPartition, setCurPartition] = useState<string>('');
|
||||
const [showMode, setShowMode] = useState('table');
|
||||
const [pageIndex, setPageIndex] = useState(1);
|
||||
const [pageTotal, setPageTotal] = useState(1);
|
||||
const [pageSize, setPageSize] = useState(10);
|
||||
const [consumerListLoading, setConsumerListLoading] = useState(true);
|
||||
const [consumerChartLoading, setConsumerChartLoading] = useState(false);
|
||||
// const [hashData, setHashData] = useState<HashData>({ groupName: '', topicName: '' });
|
||||
// const [visible, setVisible] = useState(false);
|
||||
const [sortObj, setSortObj] = useState<{
|
||||
sortField: string;
|
||||
sortType: 'desc' | 'asc' | '';
|
||||
}>({ sortField: '', sortType: '' });
|
||||
const [pagination, setPagination] = useState<any>({
|
||||
current: 1,
|
||||
pageSize: 10,
|
||||
position: 'bottomRight',
|
||||
showSizeChanger: true,
|
||||
pageSizeOptions: ['10', '20', '50', '100', '200', '500'],
|
||||
showTotal: (total: number) => `共 ${total} 条目`,
|
||||
});
|
||||
const clusterId = Number(params.clusterId);
|
||||
const columns = [
|
||||
{
|
||||
title: 'Topic Partition',
|
||||
dataIndex: 'partitionId',
|
||||
key: 'partitionId',
|
||||
lineClampOne: true,
|
||||
needTooltip: true,
|
||||
width: 180,
|
||||
render: (v: string, record: any) => {
|
||||
return `${record.topicName}-${v}`;
|
||||
},
|
||||
},
|
||||
{
|
||||
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 = ({
|
||||
hashData,
|
||||
pagination = { current: 1, pageSize: 10 },
|
||||
sorter = {},
|
||||
}: {
|
||||
hashData: HashData;
|
||||
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,
|
||||
};
|
||||
// if (sorter.sortField && sorter.sortType) {
|
||||
// params.sortField = sorter.sortField;
|
||||
// params.sortType = sorter.sortType;
|
||||
// }
|
||||
return Utils.post(
|
||||
Api.getTopicGroupMetric({
|
||||
clusterId,
|
||||
groupName: hashData.groupName,
|
||||
topicName: hashData.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 getTopicGroupPartitionsHistory = (hashData: HashData) => {
|
||||
return Utils.request(Api.getTopicGroupPartitionsHistory(clusterId, hashData.groupName), {
|
||||
params: {
|
||||
startTime: timeRange[0],
|
||||
endTime: timeRange[1],
|
||||
},
|
||||
});
|
||||
};
|
||||
const getTopicGroupMetricHistory = (partitions: Array<any>, hashData: HashData) => {
|
||||
const params = {
|
||||
aggType: 'sum',
|
||||
groupTopics: partitions?.map((p) => ({
|
||||
partition: p.value,
|
||||
topic: hashData.topicName,
|
||||
})),
|
||||
group: hashData.groupName,
|
||||
metricsNames: metricWithType.map((item) => item.metricName),
|
||||
startTime: timeRange[0],
|
||||
endTime: timeRange[1],
|
||||
topNu: 0,
|
||||
};
|
||||
Utils.post(Api.getTopicGroupMetricHistory(clusterId), params).then((data: Array<MetricData>) => {
|
||||
setAllGroupMetricsData(data);
|
||||
});
|
||||
};
|
||||
const getConsumersMetadata = (hashData: HashData) => {
|
||||
return Utils.request(Api.getConsumersMetadata(clusterId, hashData.groupName, hashData.topicName));
|
||||
};
|
||||
|
||||
const getTopicsMetaData = (hashData: HashData) => {
|
||||
return Utils.request(Api.getTopicsMetaData(hashData.topicName, clusterId));
|
||||
};
|
||||
|
||||
const onClose = () => {
|
||||
setVisible(false);
|
||||
setSortObj({
|
||||
sortField: '',
|
||||
sortType: '',
|
||||
});
|
||||
// clean hash'
|
||||
// scene === 'topicDetail' && history.goBack();
|
||||
// scene !== 'topicDetail' && window.history.pushState('', '', location.pathname);
|
||||
};
|
||||
|
||||
const onTableChange = (pagination: any, filters: any, sorter: any) => {
|
||||
getTopicGroupMetric({ hashData, pagination, sorter });
|
||||
// setPageIndex(pagination.current);
|
||||
};
|
||||
|
||||
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) => ({
|
||||
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]);
|
||||
|
||||
useEffect(() => {
|
||||
// const hashData = hashDataParse(location.hash);
|
||||
if (!hashData.groupName || !hashData.topicName) return;
|
||||
// setHashData(hashData);
|
||||
// 获取分区列表 为图表模式做准备
|
||||
visible &&
|
||||
getConsumersMetadata(hashData).then((res: any) => {
|
||||
if (!res.exist) {
|
||||
setVisible(false);
|
||||
// history.push(`/cluster/${params?.clusterId}/consumers`);
|
||||
return;
|
||||
}
|
||||
getTopicsMetaData(hashData)
|
||||
// .then((data: any) => {
|
||||
// if (data.length > 0) {
|
||||
// setCurPartition(data[0].partition);
|
||||
// }
|
||||
// setPartitionList(data);
|
||||
// return data;
|
||||
// })
|
||||
.then((data: any) => {
|
||||
const partitionLists = (data?.partitionIdList || []).map((item: any) => {
|
||||
return {
|
||||
label: item,
|
||||
value: item,
|
||||
};
|
||||
});
|
||||
setCurPartition(partitionLists?.[0]?.value);
|
||||
setPartitionList(partitionLists);
|
||||
getTopicGroupMetricHistory(partitionLists, hashData);
|
||||
})
|
||||
.catch((e) => {
|
||||
// history.push(`/cluster/${params?.clusterId}/consumers`);
|
||||
setVisible(false);
|
||||
});
|
||||
// 获取Consumer列表 表格模式
|
||||
getTopicGroupMetric({ hashData: hashData as HashData });
|
||||
});
|
||||
}, [visible]);
|
||||
|
||||
useEffect(() => {
|
||||
if (partitionList.length === 0) return;
|
||||
getTopicGroupMetricHistory(partitionList, hashData);
|
||||
}, [timeRange]);
|
||||
|
||||
return (
|
||||
<Drawer
|
||||
push={false}
|
||||
title="Consumer Group详情"
|
||||
width={1080}
|
||||
placement="right"
|
||||
onClose={onClose}
|
||||
visible={visible}
|
||||
className="consumer-group-detail-drawer"
|
||||
maskClosable={false}
|
||||
destroyOnClose
|
||||
extra={
|
||||
<Space>
|
||||
{global.hasPermission &&
|
||||
global.hasPermission(
|
||||
scene === 'topicDetail' ? ClustersPermissionMap.TOPIC_RESET_OFFSET : ClustersPermissionMap.CONSUMERS_RESET_OFFSET
|
||||
) && <ResetOffsetDrawer record={hashData}></ResetOffsetDrawer>}
|
||||
<Divider type="vertical" />
|
||||
</Space>
|
||||
}
|
||||
>
|
||||
<div className="consumer-group-detail">
|
||||
<div className="title-and-mode">
|
||||
<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' && (
|
||||
// <Table
|
||||
// rowKey={'partitionId'}
|
||||
// columns={columns}
|
||||
// className="table"
|
||||
// loading={consumerListLoading}
|
||||
// dataSource={consumerList}
|
||||
// pagination={{
|
||||
// current: pageIndex,
|
||||
// pageSize: pageSize,
|
||||
// total: pageTotal,
|
||||
// simple: true
|
||||
// }}
|
||||
// onChange={(pagination: any, filters: any, sorter: any) => {
|
||||
// setSortObj({
|
||||
// sortField: sorter.field || '',
|
||||
// sortType: sorter.order ? sorter.order.substring(0, sorter.order.indexOf('end')) : '',
|
||||
// });
|
||||
// setPageIndex(pagination.current);
|
||||
// }}
|
||||
// ></Table>
|
||||
<ProTable
|
||||
showQueryForm={false}
|
||||
tableProps={{
|
||||
loading: consumerListLoading,
|
||||
showHeader: false,
|
||||
rowKey: 'partitionId',
|
||||
columns: columns,
|
||||
dataSource: consumerList,
|
||||
paginationProps: { ...pagination },
|
||||
attrs: {
|
||||
sortDirections: ['descend', 'ascend', 'default'],
|
||||
scroll: { x: 1032 },
|
||||
// className: 'frameless-table', // 纯无边框表格类名
|
||||
onChange: onTableChange,
|
||||
bordered: false,
|
||||
},
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
{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"
|
||||
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>
|
||||
</Drawer>
|
||||
);
|
||||
};
|
||||
@@ -1,50 +1,58 @@
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import { ProTable, Utils } from 'knowdesign';
|
||||
import Api from '@src/api';
|
||||
import { useParams } from 'react-router-dom';
|
||||
import ConsumerGroupDetail from './ConsumerGroupDetail';
|
||||
const { request } = Utils;
|
||||
const getColmns = (solveClick: any) => {
|
||||
const columns = [
|
||||
|
||||
const getColmns = (arg: any) => {
|
||||
const baseColumns: any = [
|
||||
{
|
||||
title: 'ConsumerGroup',
|
||||
dataIndex: 'ConsumerGroup',
|
||||
key: 'ConsumerGroup',
|
||||
dataIndex: 'groupName',
|
||||
key: 'groupName',
|
||||
render: (v: any, r: any) => {
|
||||
return <a onClick={() => arg.getGroupInfo(v)}>{v}</a>;
|
||||
},
|
||||
},
|
||||
{
|
||||
title: '关联KafkaUser',
|
||||
dataIndex: 'kafkaUser',
|
||||
key: 'kafkaUser',
|
||||
title: '消费的Topic',
|
||||
dataIndex: 'topicName',
|
||||
key: 'topicName',
|
||||
},
|
||||
// {
|
||||
// title: 'Principle',
|
||||
// dataIndex: 'kafkaUser',
|
||||
// key: 'kafkaUser',
|
||||
// },
|
||||
{
|
||||
title: 'Status',
|
||||
dataIndex: 'status',
|
||||
key: 'status',
|
||||
dataIndex: 'state',
|
||||
key: 'state',
|
||||
},
|
||||
{
|
||||
title: 'Max Lag',
|
||||
dataIndex: 'maxLag',
|
||||
key: 'maxLag',
|
||||
render: (t: number) => (t ? t.toLocaleString() : '-'),
|
||||
},
|
||||
{
|
||||
title: 'Member数',
|
||||
dataIndex: 'member',
|
||||
key: 'member',
|
||||
},
|
||||
{
|
||||
title: '操作',
|
||||
dataIndex: 'option',
|
||||
key: 'option',
|
||||
// eslint-disable-next-line react/display-name
|
||||
render: (_t: any, r: any) => {
|
||||
return <a onClick={() => solveClick(r)}>解决</a>;
|
||||
},
|
||||
dataIndex: 'memberCount',
|
||||
key: 'memberCount',
|
||||
render: (t: number) => (t ? t.toLocaleString() : '-'),
|
||||
},
|
||||
];
|
||||
return columns;
|
||||
|
||||
return baseColumns;
|
||||
};
|
||||
|
||||
const TopicGroup = (props: any) => {
|
||||
const { hashData } = props;
|
||||
const urlParams = useParams<any>(); // 获取地址栏参数
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [data, setData] = useState([]);
|
||||
const [visible, setVisible] = useState(false);
|
||||
const [pagination, setPagination] = useState<any>({
|
||||
current: 1,
|
||||
pageSize: 10,
|
||||
@@ -52,32 +60,22 @@ const TopicGroup = (props: any) => {
|
||||
showSizeChanger: true,
|
||||
pageSizeOptions: ['10', '20', '50', '100', '200', '500'],
|
||||
showTotal: (total: number) => `共 ${total} 条目`,
|
||||
// locale: {
|
||||
// items_per_page: '条',
|
||||
// },
|
||||
// selectComponentClass: CustomSelect,
|
||||
});
|
||||
const solveClick = (record: any) => {};
|
||||
const [groupName, setGroupName] = useState('');
|
||||
|
||||
// 请求接口获取数据
|
||||
const genData = async ({ pageNo, pageSize, filters = null, sorter = null }: any) => {
|
||||
// if (clusterId === undefined) return;
|
||||
|
||||
// filters = filters || filteredInfo;
|
||||
if (urlParams?.clusterId === undefined || hashData?.topicName === undefined) return;
|
||||
|
||||
setLoading(true);
|
||||
// const params = dealTableRequestParams({ searchKeywords, pageNo, pageSize, sorter, filters, isPhyId: true });
|
||||
|
||||
const params = {
|
||||
filterKey: 'string',
|
||||
filterPartitionId: 1,
|
||||
filterValue: 'string',
|
||||
maxRecords: 100,
|
||||
pullTimeoutUnitMs: 10000,
|
||||
truncate: true,
|
||||
searchGroupName: props.searchKeywords ? props.searchKeywords.slice(0, 128) : undefined,
|
||||
pageNo,
|
||||
pageSize,
|
||||
};
|
||||
|
||||
request(Api.getTopicMessagesList('你好', 2), { params })
|
||||
request(Api.getTopicGroupList(hashData?.topicName, urlParams?.clusterId), { params })
|
||||
.then((res: any) => {
|
||||
setPagination({
|
||||
current: res.pagination?.pageNo,
|
||||
@@ -97,38 +95,62 @@ const TopicGroup = (props: any) => {
|
||||
});
|
||||
};
|
||||
|
||||
const onTableChange = (pagination: any, filters: any, sorter: any) => {
|
||||
setPagination(pagination);
|
||||
// const asc = sorter?.order && sorter?.order === 'ascend' ? true : false;
|
||||
// const sortColumn = sorter.field && toLine(sorter.field);
|
||||
// genData({ pageNo: pagination.current, pageSize: pagination.pageSize, filters, asc, sortColumn, queryTerm: searchResult, ...allParams });
|
||||
const getGroupInfo = (groupName: string) => {
|
||||
setVisible(true);
|
||||
setGroupName(groupName);
|
||||
};
|
||||
|
||||
// useEffect(() => {
|
||||
// genData({
|
||||
// pageNo: 1,
|
||||
// pageSize: pagination.pageSize,
|
||||
// // sorter: defaultSorter
|
||||
// });
|
||||
// }, [props]);
|
||||
const onTableChange = (pagination: any, filters: any, sorter: any) => {
|
||||
// setPagination(pagination);
|
||||
// const asc = sorter?.order && sorter?.order === 'ascend' ? true : false;
|
||||
// const sortColumn = sorter.field && toLine(sorter.field);
|
||||
genData({ pageNo: pagination.current, pageSize: pagination.pageSize });
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
props.positionType === 'ConsumerGroups' &&
|
||||
genData({
|
||||
pageNo: 1,
|
||||
pageSize: pagination.pageSize,
|
||||
// sorter: defaultSorter
|
||||
});
|
||||
}, [props.searchKeywords]);
|
||||
|
||||
return (
|
||||
<ProTable
|
||||
showQueryForm={false}
|
||||
tableProps={{
|
||||
showHeader: false,
|
||||
rowKey: 'path',
|
||||
loading: loading,
|
||||
columns: getColmns(solveClick),
|
||||
dataSource: data,
|
||||
paginationProps: { ...pagination },
|
||||
attrs: {
|
||||
// className: 'frameless-table', // 纯无边框表格类名
|
||||
bordered: false,
|
||||
onChange: onTableChange,
|
||||
},
|
||||
}}
|
||||
/>
|
||||
<div className="pro-table-wrap">
|
||||
<ProTable
|
||||
showQueryForm={false}
|
||||
tableProps={{
|
||||
loading,
|
||||
showHeader: false,
|
||||
rowKey: 'unique',
|
||||
columns: getColmns({ getGroupInfo }),
|
||||
dataSource: data,
|
||||
paginationProps: { ...pagination },
|
||||
attrs: {
|
||||
onChange: onTableChange,
|
||||
scroll: { y: 'calc(100vh - 400px)' },
|
||||
},
|
||||
}}
|
||||
/>
|
||||
{<ConsumerGroupDetail scene={'topicDetail'} visible={visible} setVisible={setVisible} hashData={{ ...hashData, groupName }} />}
|
||||
</div>
|
||||
// <ProTable
|
||||
// showQueryForm={false}
|
||||
// tableProps={{
|
||||
// showHeader: false,
|
||||
// rowKey: 'path',
|
||||
// loading: loading,
|
||||
// columns: getColmns(solveClick),
|
||||
// dataSource: data,
|
||||
// paginationProps: { ...pagination },
|
||||
// attrs: {
|
||||
// // className: 'frameless-table', // 纯无边框表格类名
|
||||
// bordered: false,
|
||||
// onChange: onTableChange,
|
||||
// },
|
||||
// }}
|
||||
// />
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
@@ -0,0 +1,197 @@
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import { Button, DatePicker, Drawer, Form, notification, Radio, Utils, Space, Divider, message } from 'knowdesign';
|
||||
import { useParams } from 'react-router-dom';
|
||||
import EditTable from '../TestingProduce/component/EditTable';
|
||||
import Api from '@src/api/index';
|
||||
import moment from 'moment';
|
||||
|
||||
const CustomSelectResetTime = (props: { value?: string; onChange?: (val: Number | String) => void }) => {
|
||||
const { value, onChange } = props;
|
||||
const [timeSetMode, setTimeSetMode] = useState('newest');
|
||||
useEffect(() => {
|
||||
onChange('newest');
|
||||
}, []);
|
||||
return (
|
||||
<>
|
||||
<Radio.Group
|
||||
style={{
|
||||
marginBottom: 20,
|
||||
}}
|
||||
onChange={(e) => {
|
||||
setTimeSetMode(e.target.value);
|
||||
if (e.target.value === 'newest') {
|
||||
onChange('newest');
|
||||
}
|
||||
}}
|
||||
value={timeSetMode}
|
||||
>
|
||||
<Radio value={'newest'}>最新Offset</Radio>
|
||||
<Radio value={'custom'}>自定义</Radio>
|
||||
</Radio.Group>
|
||||
{timeSetMode === 'custom' && (
|
||||
<DatePicker
|
||||
value={moment(value === 'newest' ? Date.now() : value)}
|
||||
style={{ width: '100%' }}
|
||||
showTime={true}
|
||||
onChange={(v) => {
|
||||
onChange(v.valueOf());
|
||||
}}
|
||||
></DatePicker>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default (props: any) => {
|
||||
const { record } = props;
|
||||
const routeParams = useParams<{
|
||||
clusterId: string;
|
||||
}>();
|
||||
const [form] = Form.useForm();
|
||||
const defaultResetType = 'assignedTime';
|
||||
const [resetType, setResetType] = useState(defaultResetType);
|
||||
const [resetOffsetVisible, setResetOffsetVisible] = useState(false);
|
||||
const customFormRef: any = React.createRef();
|
||||
const clusterPhyId = Number(routeParams.clusterId);
|
||||
const [partitionIdList, setPartitionIdList] = useState([]);
|
||||
useEffect(() => {
|
||||
form.setFieldsValue({
|
||||
resetType: defaultResetType,
|
||||
});
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
Utils.request(Api.getTopicsMetaData(record?.topicName, +routeParams.clusterId))
|
||||
.then((res: any) => {
|
||||
const partitionLists = (res?.partitionIdList || []).map((item: any) => {
|
||||
return {
|
||||
label: item,
|
||||
value: item,
|
||||
};
|
||||
});
|
||||
setPartitionIdList(partitionLists);
|
||||
})
|
||||
.catch((err) => {
|
||||
message.error(err);
|
||||
});
|
||||
}, []);
|
||||
const confirm = () => {
|
||||
let tableData;
|
||||
if (customFormRef.current) {
|
||||
tableData = customFormRef.current.getTableData();
|
||||
}
|
||||
const formData = form.getFieldsValue();
|
||||
let resetParams: any = {
|
||||
clusterId: clusterPhyId,
|
||||
createIfNotExist: false,
|
||||
groupName: record.groupName,
|
||||
topicName: record.topicName,
|
||||
};
|
||||
if (formData.resetType === 'assignedTime') {
|
||||
resetParams.resetType = formData.timestamp === 'newest' ? 0 : 2;
|
||||
if (resetParams.resetType === 2) {
|
||||
resetParams.timestamp = formData.timestamp;
|
||||
}
|
||||
}
|
||||
if (formData.resetType === 'partition') {
|
||||
resetParams.resetType = 3;
|
||||
resetParams.offsetList = tableData
|
||||
? tableData.map((item: { key: string; value: string }) => ({ partitionId: item.key, offset: item.value }))
|
||||
: [];
|
||||
}
|
||||
Utils.put(Api.resetGroupOffset(), resetParams).then((data) => {
|
||||
if (data === null) {
|
||||
notification.success({
|
||||
message: '重置offset成功',
|
||||
});
|
||||
setResetOffsetVisible(false);
|
||||
} else {
|
||||
notification.error({
|
||||
message: '重置offset失败',
|
||||
});
|
||||
setResetOffsetVisible(false);
|
||||
}
|
||||
});
|
||||
};
|
||||
return (
|
||||
<>
|
||||
<Button
|
||||
size="small"
|
||||
type="primary"
|
||||
onClick={(_) => {
|
||||
setResetOffsetVisible(true);
|
||||
}}
|
||||
>
|
||||
重置Offset
|
||||
</Button>
|
||||
|
||||
<Drawer
|
||||
title="重置Offset"
|
||||
width={480}
|
||||
visible={resetOffsetVisible}
|
||||
maskClosable={false}
|
||||
extra={
|
||||
<Space>
|
||||
<Button
|
||||
size="small"
|
||||
style={{ marginRight: 8 }}
|
||||
onClick={(_) => {
|
||||
setResetOffsetVisible(false);
|
||||
}}
|
||||
>
|
||||
取消
|
||||
</Button>
|
||||
<Button size="small" type="primary" onClick={confirm}>
|
||||
确定
|
||||
</Button>
|
||||
<Divider type="vertical" />
|
||||
</Space>
|
||||
}
|
||||
className="cluster-detail-consumer-resetoffset"
|
||||
onClose={(_) => {
|
||||
setResetOffsetVisible(false);
|
||||
}}
|
||||
>
|
||||
<Form form={form} labelCol={{ span: 5 }} layout="vertical" className="reset-offset-form">
|
||||
<Form.Item name="resetType" label="重置类型" required>
|
||||
<Radio.Group
|
||||
defaultValue="assignedTime"
|
||||
value={resetType}
|
||||
onChange={(e) => {
|
||||
setResetType(e.target.value);
|
||||
}}
|
||||
>
|
||||
<Radio value={'assignedTime'}>重置到指定时间</Radio>
|
||||
<Radio value={'partition'}>重置分区</Radio>
|
||||
</Radio.Group>
|
||||
</Form.Item>
|
||||
{resetType === 'assignedTime' && (
|
||||
<Form.Item name="timestamp" label="时间" required>
|
||||
<CustomSelectResetTime />
|
||||
</Form.Item>
|
||||
)}
|
||||
{resetType === 'partition' && (
|
||||
<Form.Item name="partition" label="分区及偏移" required>
|
||||
<EditTable
|
||||
ref={customFormRef}
|
||||
colCustomConfigs={[
|
||||
{
|
||||
title: 'PartitionID',
|
||||
inputType: 'select',
|
||||
placeholder: '请输入Partition',
|
||||
options: partitionIdList,
|
||||
},
|
||||
{
|
||||
title: 'Offset',
|
||||
inputType: 'number',
|
||||
placeholder: '请输入Offset',
|
||||
},
|
||||
]}
|
||||
></EditTable>
|
||||
</Form.Item>
|
||||
)}
|
||||
</Form>
|
||||
</Drawer>
|
||||
</>
|
||||
);
|
||||
};
|
||||
@@ -1,13 +1,16 @@
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import { useHistory, useParams } from 'react-router-dom';
|
||||
import { Tabs, Utils, Drawer, Tag, AppContainer, SearchInput, notification } from 'knowdesign';
|
||||
import { Tabs, Utils, Drawer, Tag, AppContainer, SearchInput } from 'knowdesign';
|
||||
import notification from '@src/components/Notification';
|
||||
|
||||
import Api from '@src/api';
|
||||
import BrokersDetail from './BrokersDetail';
|
||||
import Messages from './Messages';
|
||||
import ConsumerGroups from './ConsumerGroups';
|
||||
import ACLs from './ACLs';
|
||||
import Configuration from './Configuration';
|
||||
import Consumers from '@src/pages/Consumers';
|
||||
import Consumers from './ConsumerGroups';
|
||||
// import Consumers from '@src/pages/Consumers';
|
||||
import './index.less';
|
||||
import TopicDetailHealthCheck from '@src/components/CardBar/TopicDetailHealthCheck';
|
||||
import { hashDataParse } from '@src/constants/common';
|
||||
@@ -115,35 +118,34 @@ const TopicDetail = (props: any) => {
|
||||
useEffect(() => {
|
||||
global?.clusterInfo?.id && hashDataParse(location.hash).topicName
|
||||
? Utils.request(Api.getTopicMetadata(+global?.clusterInfo?.id, hashDataParse(location.hash)?.topicName), {
|
||||
init: {
|
||||
errorNoTips: true,
|
||||
},
|
||||
})
|
||||
.then((topicData: any) => {
|
||||
if (topicData?.exist && !hashDataParse(location.hash).groupName) {
|
||||
setHashData(topicData);
|
||||
setVisible(true);
|
||||
} else {
|
||||
init: {
|
||||
errorNoTips: true,
|
||||
},
|
||||
})
|
||||
.then((topicData: any) => {
|
||||
if (topicData?.exist && !hashDataParse(location.hash).groupName) {
|
||||
setHashData(topicData);
|
||||
setVisible(true);
|
||||
} else {
|
||||
history.replace(`/cluster/${urlParams?.clusterId}/topic/list`);
|
||||
// history.push(`/`);
|
||||
setVisible(false);
|
||||
}
|
||||
})
|
||||
.catch((err) => {
|
||||
history.replace(`/cluster/${urlParams?.clusterId}/topic/list`);
|
||||
// history.push(`/`);
|
||||
setVisible(false);
|
||||
}
|
||||
})
|
||||
.catch((err) => {
|
||||
history.replace(`/cluster/${urlParams?.clusterId}/topic/list`);
|
||||
setVisible(false);
|
||||
notification.error({
|
||||
message: '错误',
|
||||
duration: 3,
|
||||
description: `${'Topic不存在或Topic名称有误'}`,
|
||||
});
|
||||
})
|
||||
notification.error({
|
||||
message: '错误',
|
||||
description: 'Topic不存在或Topic名称有误',
|
||||
});
|
||||
})
|
||||
: setVisible(false);
|
||||
}, [hashDataParse(location.hash).topicName, global?.clusterInfo]);
|
||||
|
||||
return (
|
||||
<Drawer
|
||||
push={false}
|
||||
// push={false}
|
||||
title={
|
||||
<span>
|
||||
<span style={{ fontSize: '18px', fontFamily: 'PingFangSC-Semibold', color: '#495057' }}>{hashData?.topicName}</span>
|
||||
@@ -192,12 +194,9 @@ const TopicDetail = (props: any) => {
|
||||
{positionType === 'Messages' && <Messages searchKeywords={searchKeywords} positionType={positionType} hashData={hashData} />}
|
||||
</TabPane>
|
||||
<TabPane tab="ConsumerGroups" key="ConsumerGroups">
|
||||
<Consumers
|
||||
scene="topicDetail"
|
||||
detailParams={{
|
||||
searchKeywords,
|
||||
}}
|
||||
></Consumers>
|
||||
{positionType === 'ConsumerGroups' && (
|
||||
<Consumers searchKeywords={searchKeywords} positionType={positionType} hashData={hashData} />
|
||||
)}
|
||||
</TabPane>
|
||||
<TabPane tab="ACLs" key="ACLs">
|
||||
{positionType === 'ACLs' && <ACLs searchKeywords={searchKeywords} positionType={positionType} hashData={hashData} />}
|
||||
|
||||
@@ -10,7 +10,7 @@ import BrokerControllerChangeLog from './BrokerControllerChangeLog';
|
||||
import TopicBoard from './TopicDashboard';
|
||||
import TopicList from './TopicList';
|
||||
|
||||
import Consumers from './Consumers/index';
|
||||
import Consumers from './ConsumerGroup';
|
||||
|
||||
import Jobs from './Jobs';
|
||||
|
||||
|
||||
Reference in New Issue
Block a user