mirror of
https://github.com/didi/KnowStreaming.git
synced 2025-12-24 03:42:07 +08:00
feat: 新增Mirror Maker 2.0(MM2)
This commit is contained in:
@@ -18,6 +18,7 @@ export enum MetricType {
|
||||
Connect = 120,
|
||||
Connectors = 121,
|
||||
Controls = 901,
|
||||
MM2 = 122,
|
||||
}
|
||||
|
||||
const api = {
|
||||
@@ -233,9 +234,9 @@ const api = {
|
||||
getConnectors: (clusterPhyId: string) => getApi(`/clusters/${clusterPhyId}/connectors-basic`),
|
||||
getConnectorMetrics: (clusterPhyId: string) => getApi(`/clusters/${clusterPhyId}/connectors-metrics`),
|
||||
getConnectorPlugins: (connectClusterId: number) => getApi(`/kafka-connect/clusters/${connectClusterId}/connector-plugins`),
|
||||
getConnectorPluginConfig: (connectClusterId: number, pluginName: string) =>
|
||||
getConnectorPluginConfig: (connectClusterId: number | string, pluginName: string) =>
|
||||
getApi(`/kafka-connect/clusters/${connectClusterId}/connector-plugins/${pluginName}/config`),
|
||||
getCurPluginConfig: (connectClusterId: number, connectorName: string) =>
|
||||
getCurPluginConfig: (connectClusterId: number | string, connectorName: string) =>
|
||||
getApi(`/kafka-connect/clusters/${connectClusterId}/connectors/${connectorName}/config`),
|
||||
isConnectorExist: (connectClusterId: number, connectorName: string) =>
|
||||
getApi(`/kafka-connect/clusters/${connectClusterId}/connectors/${connectorName}/basic-combine-exist`),
|
||||
@@ -251,6 +252,39 @@ const api = {
|
||||
|
||||
getConnectClusterBasicExit: (clusterPhyId: string, clusterPhyName: string) =>
|
||||
getApi(`/kafka-clusters/${clusterPhyId}/connect-clusters/${clusterPhyName}/basic-combine-exist`),
|
||||
|
||||
// MM2 列表
|
||||
getMirrorMakerList: (clusterPhyId: number) => getApi(`/clusters/${clusterPhyId}/mirror-makers-overview`),
|
||||
// MM2 状态卡片
|
||||
getMirrorMakerState: (clusterPhyId: string) => getApi(`/kafka-clusters/${clusterPhyId}/mirror-makers-state`),
|
||||
// MM2 指标卡片
|
||||
getMirrorMakerMetrics: (clusterPhyId: string) => getApi(`/clusters/${clusterPhyId}/mirror-makers-metrics`),
|
||||
// MM2 筛选
|
||||
getMirrorMakerMetadata: (clusterPhyId: string) => getApi(`/clusters/${clusterPhyId}/mirror-makers-basic`),
|
||||
// MM2 详情列表
|
||||
getMM2DetailTasks: (connectorName: number | string, connectClusterId: number | string) =>
|
||||
getApi(`/kafka-mm2/clusters/${connectClusterId}/connectors/${connectorName}/tasks`),
|
||||
// MM2 详情状态卡片
|
||||
getMM2DetailState: (connectorName: number | string, connectClusterId: number | string) =>
|
||||
getApi(`/kafka-mm2/clusters/${connectClusterId}/connectors/${connectorName}/state`),
|
||||
// MM2 操作接口 新增、暂停、重启、删除
|
||||
mirrorMakerOperates: getApi('/kafka-mm2/mirror-makers'),
|
||||
// MM2 操作接口 新增、编辑校验
|
||||
validateMM2Config: getApi('/kafka-mm2/mirror-makers-config/validate'),
|
||||
// 修改 Connector 配置
|
||||
updateMM2Config: getApi('/kafka-mm2/mirror-makers-config'),
|
||||
// MM2 详情
|
||||
getMirrorMakerMetricPoints: (mirrorMakerName: number | string, connectClusterId: number | string) =>
|
||||
getApi(`/kafka-mm2/clusters/${connectClusterId}/connectors/${mirrorMakerName}/latest-metrics`),
|
||||
getSourceKafkaClusterBasic: getApi(`/physical-clusters/basic`),
|
||||
getGroupBasic: (clusterPhyId: string) => getApi(`/clusters/${clusterPhyId}/groups-basic`),
|
||||
// Topic复制
|
||||
getMirrorClusterList: () => getApi(`/ha-mirror/physical-clusters/basic`),
|
||||
handleTopicMirror: () => getApi(`/ha-mirror/topics`),
|
||||
getTopicMirrorList: (clusterPhyId: number, topicName: string) =>
|
||||
getApi(`/ha-mirror/clusters/${clusterPhyId}/topics/${topicName}/mirror-info`),
|
||||
getMirrorMakerConfig: (connectClusterId: number | string, connectorName: string) =>
|
||||
getApi(`/kafka-mm2/clusters/${connectClusterId}/connectors/${connectorName}/config`),
|
||||
};
|
||||
|
||||
export default api;
|
||||
|
||||
@@ -0,0 +1,119 @@
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import { useParams } from 'react-router-dom';
|
||||
import CardBar, { healthDataProps } from './index';
|
||||
import { Tooltip, Utils } from 'knowdesign';
|
||||
import api from '@src/api';
|
||||
import { HealthStateEnum } from '../HealthState';
|
||||
import { InfoCircleOutlined } from '@ant-design/icons';
|
||||
|
||||
interface MM2State {
|
||||
workerCount: number;
|
||||
aliveConnectorCount: number;
|
||||
aliveTaskCount: number;
|
||||
healthCheckPassed: number;
|
||||
healthCheckTotal: number;
|
||||
healthState: number;
|
||||
totalConnectorCount: string;
|
||||
totalTaskCount: number;
|
||||
totalServerCount: number;
|
||||
mirrorMakerCount: number;
|
||||
}
|
||||
|
||||
const getVal = (val: string | number | undefined | null) => {
|
||||
return val === undefined || val === null || val === '' ? '0' : val;
|
||||
};
|
||||
|
||||
const ConnectCard = ({ state }: { state?: boolean }) => {
|
||||
const { clusterId } = useParams<{
|
||||
clusterId: string;
|
||||
}>();
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [cardData, setCardData] = useState([]);
|
||||
const [healthData, setHealthData] = useState<healthDataProps>({
|
||||
state: HealthStateEnum.UNKNOWN,
|
||||
passed: 0,
|
||||
total: 0,
|
||||
});
|
||||
|
||||
const getHealthData = () => {
|
||||
return Utils.post(api.getMetricPointsLatest(Number(clusterId)), [
|
||||
'HealthCheckPassed_MirrorMaker',
|
||||
'HealthCheckTotal_MirrorMaker',
|
||||
'HealthState_MirrorMaker',
|
||||
]).then((data: any) => {
|
||||
setHealthData({
|
||||
state: data?.metrics?.['HealthState_MirrorMaker'],
|
||||
passed: data?.metrics?.['HealthCheckPassed_MirrorMaker'] || 0,
|
||||
total: data?.metrics?.['HealthCheckTotal_MirrorMaker'] || 0,
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
const getCardInfo = () => {
|
||||
return Utils.request(api.getMirrorMakerState(clusterId)).then((res: MM2State) => {
|
||||
const { mirrorMakerCount, aliveConnectorCount, aliveTaskCount, totalConnectorCount, totalTaskCount, workerCount } = res || {};
|
||||
const cardMap = [
|
||||
{
|
||||
title: 'MM2s',
|
||||
value: getVal(mirrorMakerCount),
|
||||
customStyle: {
|
||||
// 自定义cardbar样式
|
||||
marginLeft: 0,
|
||||
},
|
||||
},
|
||||
{
|
||||
title: 'Workers',
|
||||
value: getVal(workerCount),
|
||||
},
|
||||
{
|
||||
title() {
|
||||
return (
|
||||
<div>
|
||||
<span style={{ display: 'inline-block', marginRight: '8px' }}>Connectors</span>
|
||||
<Tooltip overlayClassName="rebalance-tooltip" title="conector运行数/总数">
|
||||
<InfoCircleOutlined />
|
||||
</Tooltip>
|
||||
</div>
|
||||
);
|
||||
},
|
||||
value() {
|
||||
return (
|
||||
<span>
|
||||
{getVal(aliveConnectorCount)}/{getVal(totalConnectorCount)}
|
||||
</span>
|
||||
);
|
||||
},
|
||||
},
|
||||
{
|
||||
title() {
|
||||
return (
|
||||
<div>
|
||||
<span style={{ display: 'inline-block', marginRight: '8px' }}>Tasks</span>
|
||||
<Tooltip overlayClassName="rebalance-tooltip" title="Task运行数/总数">
|
||||
<InfoCircleOutlined />
|
||||
</Tooltip>
|
||||
</div>
|
||||
);
|
||||
},
|
||||
value() {
|
||||
return (
|
||||
<span>
|
||||
{getVal(aliveTaskCount)}/{getVal(totalTaskCount)}
|
||||
</span>
|
||||
);
|
||||
},
|
||||
},
|
||||
];
|
||||
setCardData(cardMap);
|
||||
});
|
||||
};
|
||||
useEffect(() => {
|
||||
setLoading(true);
|
||||
Promise.all([getHealthData(), getCardInfo()]).finally(() => {
|
||||
setLoading(false);
|
||||
});
|
||||
}, [clusterId, state]);
|
||||
return <CardBar scene="mm2" healthData={healthData} cardColumns={cardData} loading={loading}></CardBar>;
|
||||
};
|
||||
|
||||
export default ConnectCard;
|
||||
@@ -0,0 +1,145 @@
|
||||
/* eslint-disable react/display-name */
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import { useLocation, useParams } from 'react-router-dom';
|
||||
import CardBar from '@src/components/CardBar';
|
||||
import { healthDataProps } from '.';
|
||||
import { Tooltip, Utils } from 'knowdesign';
|
||||
import Api from '@src/api';
|
||||
import { hashDataParse } from '@src/constants/common';
|
||||
import { HealthStateEnum } from '../HealthState';
|
||||
import { InfoCircleOutlined } from '@ant-design/icons';
|
||||
import { stateEnum } from '@src/pages/Connect/config';
|
||||
const getVal = (val: string | number | undefined | null) => {
|
||||
return val === undefined || val === null || val === '' ? '0' : val;
|
||||
};
|
||||
|
||||
const ConnectDetailCard = (props: { record: any; tabSelectType: string }) => {
|
||||
const { record, tabSelectType } = props;
|
||||
const urlParams = useParams<{ clusterId: string; brokerId: string }>();
|
||||
const urlLocation = useLocation<any>();
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [cardData, setCardData] = useState([]);
|
||||
const [healthData, setHealthData] = useState<healthDataProps>({
|
||||
state: HealthStateEnum.UNKNOWN,
|
||||
passed: 0,
|
||||
total: 0,
|
||||
});
|
||||
|
||||
const getHealthData = (tabSelectTypeName: string) => {
|
||||
return Utils.post(Api.getMirrorMakerMetricPoints(tabSelectTypeName, record?.connectClusterId), [
|
||||
'HealthState',
|
||||
'HealthCheckPassed',
|
||||
'HealthCheckTotal',
|
||||
]).then((data: any) => {
|
||||
setHealthData({
|
||||
state: data?.metrics?.['HealthState'],
|
||||
passed: data?.metrics?.['HealthCheckPassed'] || 0,
|
||||
total: data?.metrics?.['HealthCheckTotal'] || 0,
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
const getCardInfo = (tabSelectTypeName: string) => {
|
||||
return Utils.request(Api.getConnectDetailState(tabSelectTypeName, record?.connectClusterId)).then((res: any) => {
|
||||
const { type, aliveTaskCount, state, totalTaskCount, totalWorkerCount } = res || {};
|
||||
const cordRightMap = [
|
||||
{
|
||||
title: 'Status',
|
||||
// value: Utils.firstCharUppercase(state) || '-',
|
||||
value: () => {
|
||||
return (
|
||||
<>
|
||||
{
|
||||
<span style={{ fontFamily: 'HelveticaNeue-Medium', fontSize: 32, color: stateEnum[state].color }}>
|
||||
{Utils.firstCharUppercase(state) || '-'}
|
||||
</span>
|
||||
}
|
||||
</>
|
||||
);
|
||||
},
|
||||
},
|
||||
|
||||
{
|
||||
title() {
|
||||
return (
|
||||
<div>
|
||||
<span style={{ display: 'inline-block', marginRight: '8px' }}>Tasks</span>
|
||||
<Tooltip overlayClassName="rebalance-tooltip" title="Task运行数/总数">
|
||||
<InfoCircleOutlined />
|
||||
</Tooltip>
|
||||
</div>
|
||||
);
|
||||
},
|
||||
value() {
|
||||
return (
|
||||
<span>
|
||||
{getVal(aliveTaskCount)}/{getVal(totalTaskCount)}
|
||||
</span>
|
||||
);
|
||||
},
|
||||
},
|
||||
{
|
||||
title: 'Workers',
|
||||
value: getVal(totalWorkerCount),
|
||||
},
|
||||
];
|
||||
setCardData(cordRightMap);
|
||||
});
|
||||
};
|
||||
|
||||
const noDataCardInfo = () => {
|
||||
const cordRightMap = [
|
||||
{
|
||||
title: 'Status',
|
||||
// value: Utils.firstCharUppercase(state) || '-',
|
||||
value() {
|
||||
return <span>-</span>;
|
||||
},
|
||||
},
|
||||
|
||||
{
|
||||
title() {
|
||||
return (
|
||||
<div>
|
||||
<span style={{ display: 'inline-block', marginRight: '8px' }}>Tasks</span>
|
||||
<Tooltip overlayClassName="rebalance-tooltip" title="Task运行数/总数">
|
||||
<InfoCircleOutlined />
|
||||
</Tooltip>
|
||||
</div>
|
||||
);
|
||||
},
|
||||
value() {
|
||||
return <span>-/-</span>;
|
||||
},
|
||||
},
|
||||
{
|
||||
title: 'Workers',
|
||||
value() {
|
||||
return <span>-</span>;
|
||||
},
|
||||
},
|
||||
];
|
||||
setCardData(cordRightMap);
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
setLoading(true);
|
||||
|
||||
const filterCardInfo =
|
||||
tabSelectType === 'MirrorCheckpoint' && record.checkpointConnector
|
||||
? getCardInfo(record.checkpointConnector)
|
||||
: tabSelectType === 'MirrorHeatbeat' && record.heartbeatConnector
|
||||
? getCardInfo(record.heartbeatConnector)
|
||||
: tabSelectType === 'MirrorSource' && record.connectorName
|
||||
? getCardInfo(record.connectorName)
|
||||
: noDataCardInfo();
|
||||
Promise.all([getHealthData(record.connectorName), filterCardInfo]).finally(() => {
|
||||
setLoading(false);
|
||||
});
|
||||
}, [record, tabSelectType]);
|
||||
return (
|
||||
<CardBar record={record} scene="mm2" healthData={healthData} cardColumns={cardData} showCardBg={false} loading={loading}></CardBar>
|
||||
);
|
||||
};
|
||||
|
||||
export default ConnectDetailCard;
|
||||
@@ -18,7 +18,7 @@ export interface CardBarProps {
|
||||
cardColumns?: any[];
|
||||
healthData?: healthDataProps;
|
||||
showCardBg?: boolean;
|
||||
scene: 'topics' | 'brokers' | 'topic' | 'broker' | 'group' | 'zookeeper' | 'connect' | 'connector';
|
||||
scene: 'topics' | 'brokers' | 'topic' | 'broker' | 'group' | 'zookeeper' | 'connect' | 'connector' | 'mm2';
|
||||
record?: any;
|
||||
loading?: boolean;
|
||||
needProgress?: boolean;
|
||||
@@ -67,6 +67,11 @@ const sceneCodeMap = {
|
||||
fieldName: 'connectorName',
|
||||
alias: 'Connector',
|
||||
},
|
||||
mm2: {
|
||||
code: 7,
|
||||
fieldName: 'connectorName',
|
||||
alias: 'MM2',
|
||||
},
|
||||
};
|
||||
const CardColumnsItem: any = (cardItem: any) => {
|
||||
const { cardColumnsItemData, showCardBg } = cardItem;
|
||||
@@ -108,7 +113,7 @@ const CardBar = (props: CardBarProps) => {
|
||||
const sceneObj = sceneCodeMap[scene];
|
||||
const path = record
|
||||
? api.getResourceHealthDetail(
|
||||
scene === 'connector' ? Number(record?.connectClusterId) : Number(routeParams.clusterId),
|
||||
scene === 'connector' || scene === 'mm2' ? Number(record?.connectClusterId) : Number(routeParams.clusterId),
|
||||
sceneObj.code,
|
||||
record[sceneObj.fieldName]
|
||||
)
|
||||
|
||||
@@ -89,7 +89,15 @@ export const MetricSelect = forwardRef((metricSelect: MetricSelectProps, ref) =>
|
||||
|
||||
const columns = [
|
||||
{
|
||||
title: `${pathname.endsWith('/broker') ? 'Broker' : pathname.endsWith('/topic') ? 'Topic' : 'Cluster'} Metrics`,
|
||||
title: `${
|
||||
pathname.endsWith('/broker')
|
||||
? 'Broker'
|
||||
: pathname.endsWith('/topic')
|
||||
? 'Topic'
|
||||
: pathname.endsWith('/replication')
|
||||
? 'MM2'
|
||||
: 'Cluster'
|
||||
} Metrics`,
|
||||
dataIndex: 'category',
|
||||
key: 'category',
|
||||
},
|
||||
@@ -112,7 +120,7 @@ export const MetricSelect = forwardRef((metricSelect: MetricSelectProps, ref) =>
|
||||
desc,
|
||||
unit: metricDefine?.unit,
|
||||
};
|
||||
if (metricDefine.category) {
|
||||
if (metricDefine?.category) {
|
||||
if (!categoryData[metricDefine.category]) {
|
||||
categoryData[metricDefine.category] = [returnData];
|
||||
} else {
|
||||
@@ -129,11 +137,11 @@ export const MetricSelect = forwardRef((metricSelect: MetricSelectProps, ref) =>
|
||||
};
|
||||
|
||||
const formateSelectedKeys = () => {
|
||||
const newKeys = metricSelect.selectedRows;
|
||||
const newKeys = metricSelect?.selectedRows;
|
||||
const result: SelectedMetrics = {};
|
||||
const selectedCategories: string[] = [];
|
||||
|
||||
newKeys.forEach((name: string) => {
|
||||
newKeys?.forEach((name: string) => {
|
||||
const metricDefine = global.getMetricDefine(metricSelect?.metricType, name);
|
||||
if (metricDefine) {
|
||||
if (!result[metricDefine.category]) {
|
||||
|
||||
@@ -167,6 +167,9 @@ const ChartDetail = (props: ChartDetailProps) => {
|
||||
const getMetricChartData = ([startTime, endTime]: readonly [number, number]) => {
|
||||
const getQueryUrl = () => {
|
||||
switch (metricType) {
|
||||
case MetricType.MM2: {
|
||||
return api.getMirrorMakerMetrics(clusterId);
|
||||
}
|
||||
case MetricType.Connect: {
|
||||
return api.getConnectClusterMetrics(clusterId);
|
||||
}
|
||||
@@ -180,13 +183,16 @@ const ChartDetail = (props: ChartDetailProps) => {
|
||||
[MetricType.Topic]: 'topics',
|
||||
[MetricType.Connect]: 'connectClusterIdList',
|
||||
[MetricType.Connectors]: 'connectorNameList',
|
||||
[MetricType.MM2]: 'connectorNameList',
|
||||
};
|
||||
|
||||
return Utils.post(getQueryUrl(), {
|
||||
startTime,
|
||||
endTime,
|
||||
metricsNames: [metricName],
|
||||
topNu: null,
|
||||
[queryMap[metricType as keyof typeof queryMap]]: queryLines,
|
||||
[queryMap[metricType as keyof typeof queryMap]]:
|
||||
metricType === MetricType.MM2 ? queryLines.map((item) => (typeof item === 'string' ? Utils.parseJSON(item) : item)) : queryLines,
|
||||
});
|
||||
};
|
||||
|
||||
|
||||
@@ -5,6 +5,7 @@ const METRIC_DASHBOARD_REQ_MAP = {
|
||||
[MetricType.Broker]: (clusterId: string) => api.getDashboardMetricChartData(clusterId, MetricType.Broker),
|
||||
[MetricType.Topic]: (clusterId: string) => api.getDashboardMetricChartData(clusterId, MetricType.Topic),
|
||||
[MetricType.Zookeeper]: (clusterId: string) => api.getZookeeperMetrics(clusterId),
|
||||
[MetricType.MM2]: (clusterId: string) => api.getMirrorMakerMetrics(clusterId),
|
||||
};
|
||||
|
||||
export const getMetricDashboardReq = (clusterId: string, type: MetricType.Broker | MetricType.Topic | MetricType.Zookeeper) =>
|
||||
|
||||
@@ -150,18 +150,35 @@ const DraggableCharts = (props: PropsType): JSX.Element => {
|
||||
|
||||
// 获取节点范围列表
|
||||
const getScopeList = async () => {
|
||||
const res: any = await Utils.request(api.getDashboardMetadata(clusterId, dashboardType));
|
||||
const list = res.map((item: any) => {
|
||||
const res: any = await Utils.request(
|
||||
dashboardType !== MetricType.MM2 ? api.getDashboardMetadata(clusterId, dashboardType) : api.getMirrorMakerMetadata(clusterId)
|
||||
);
|
||||
const mockRes = [{ connectClusterId: 1, connectClusterName: 'connectClusterName', connectorName: 'connectorName' }];
|
||||
const list =
|
||||
res.length > 0
|
||||
? res.map((item: any) => {
|
||||
return dashboardType === MetricType.Broker
|
||||
? {
|
||||
label: item.host,
|
||||
value: item.brokerId,
|
||||
}
|
||||
: dashboardType === MetricType.MM2
|
||||
? {
|
||||
label: item.connectorName,
|
||||
value: JSON.stringify({ connectClusterId: item.connectClusterId, connectorName: item.connectorName }),
|
||||
}
|
||||
: {
|
||||
label: item.topicName,
|
||||
value: item.topicName,
|
||||
};
|
||||
})
|
||||
: mockRes.map((item) => {
|
||||
return {
|
||||
label: item.connectorName,
|
||||
value: JSON.stringify(item),
|
||||
};
|
||||
});
|
||||
|
||||
setScopeList(list);
|
||||
};
|
||||
|
||||
@@ -172,18 +189,22 @@ const DraggableCharts = (props: PropsType): JSX.Element => {
|
||||
const [startTime, endTime] = curHeaderOptions.rangeTime;
|
||||
const curTimestamp = Date.now();
|
||||
curFetchingTimestamp.current = curTimestamp;
|
||||
|
||||
const reqBody = Object.assign(
|
||||
{
|
||||
startTime,
|
||||
endTime,
|
||||
metricsNames: metricList || [],
|
||||
},
|
||||
dashboardType === MetricType.Broker || dashboardType === MetricType.Topic
|
||||
dashboardType === MetricType.Broker || dashboardType === MetricType.Topic || dashboardType === MetricType.MM2
|
||||
? {
|
||||
topNu: curHeaderOptions?.scopeData?.isTop ? curHeaderOptions.scopeData.data : null,
|
||||
[dashboardType === MetricType.Broker ? 'brokerIds' : 'topics']: curHeaderOptions?.scopeData?.isTop
|
||||
[dashboardType === MetricType.Broker ? 'brokerIds' : dashboardType === MetricType.MM2 ? 'connectorNameList' : 'topics']:
|
||||
curHeaderOptions?.scopeData?.isTop
|
||||
? null
|
||||
: dashboardType === MetricType.MM2
|
||||
? curHeaderOptions.scopeData.data?.map((item: any) => {
|
||||
return JSON.parse(item);
|
||||
})
|
||||
: curHeaderOptions.scopeData.data,
|
||||
}
|
||||
: {}
|
||||
@@ -207,10 +228,31 @@ const DraggableCharts = (props: PropsType): JSX.Element => {
|
||||
dashboardType,
|
||||
curHeaderOptions.rangeTime
|
||||
) as FormattedMetricData[];
|
||||
// todo 将指标筛选选中但是没有返回的指标插入chartData中
|
||||
const nullformattedMetricData: any = [];
|
||||
|
||||
metricList?.forEach((item) => {
|
||||
if (formattedMetricData && formattedMetricData.some((key) => item === key.metricName)) {
|
||||
nullformattedMetricData.push(null);
|
||||
} else {
|
||||
const chartData: any = {
|
||||
metricName: item,
|
||||
metricType: dashboardType,
|
||||
metricUnit: global.getMetricDefine(dashboardType, item)?.unit || '',
|
||||
metricLines: [],
|
||||
showLegend: false,
|
||||
targetUnit: undefined,
|
||||
};
|
||||
nullformattedMetricData.push(chartData);
|
||||
}
|
||||
});
|
||||
// 指标排序
|
||||
formattedMetricData.sort((a, b) => metricRankList.current.indexOf(a.metricName) - metricRankList.current.indexOf(b.metricName));
|
||||
|
||||
setMetricChartData(formattedMetricData);
|
||||
const filterNullformattedMetricData = nullformattedMetricData.filter((item: any) => item !== null);
|
||||
filterNullformattedMetricData.sort(
|
||||
(a: any, b: any) => metricRankList.current.indexOf(a?.metricName) - metricRankList.current.indexOf(b?.metricName)
|
||||
);
|
||||
setMetricChartData([...formattedMetricData, ...filterNullformattedMetricData]);
|
||||
}
|
||||
setLoading(false);
|
||||
},
|
||||
@@ -255,12 +297,15 @@ const DraggableCharts = (props: PropsType): JSX.Element => {
|
||||
useEffect(() => {
|
||||
if (metricList?.length && curHeaderOptions) {
|
||||
getMetricChartData();
|
||||
} else {
|
||||
setMetricChartData([]);
|
||||
setLoading(false);
|
||||
}
|
||||
}, [curHeaderOptions, metricList]);
|
||||
|
||||
useEffect(() => {
|
||||
// 初始化页面,获取 scope 和 metric 信息
|
||||
(dashboardType === MetricType.Broker || dashboardType === MetricType.Topic) && getScopeList();
|
||||
(dashboardType === MetricType.Broker || dashboardType === MetricType.Topic || dashboardType === MetricType.MM2) && getScopeList();
|
||||
}, []);
|
||||
|
||||
return (
|
||||
@@ -270,11 +315,24 @@ const DraggableCharts = (props: PropsType): JSX.Element => {
|
||||
hideNodeScope={dashboardType === MetricType.Zookeeper}
|
||||
openMetricFilter={() => metricFilterRef.current?.open()}
|
||||
nodeSelect={{
|
||||
name: dashboardType === MetricType.Broker ? 'Broker' : dashboardType === MetricType.Topic ? 'Topic' : 'Zookeeper',
|
||||
name:
|
||||
dashboardType === MetricType.Broker
|
||||
? 'Broker'
|
||||
: dashboardType === MetricType.Topic
|
||||
? 'Topic'
|
||||
: dashboardType === MetricType.MM2
|
||||
? 'MM2'
|
||||
: 'Zookeeper',
|
||||
customContent: (
|
||||
<SelectContent
|
||||
title={`自定义 ${
|
||||
dashboardType === MetricType.Broker ? 'Broker' : dashboardType === MetricType.Topic ? 'Topic' : 'Zookeeper'
|
||||
dashboardType === MetricType.Broker
|
||||
? 'Broker'
|
||||
: dashboardType === MetricType.Topic
|
||||
? 'Topic'
|
||||
: dashboardType === MetricType.MM2
|
||||
? 'MM2'
|
||||
: 'Zookeeper'
|
||||
} 范围`}
|
||||
list={scopeList}
|
||||
/>
|
||||
|
||||
@@ -79,7 +79,6 @@ export const leftMenus = (clusterId?: string, clusterRunState?: number) => ({
|
||||
return (
|
||||
<div className="menu-item-with-beta-tag">
|
||||
<span>{intl.formatMessage({ id: 'menu.cluster.connect' })}</span>
|
||||
<div className="beta-tag"></div>
|
||||
</div>
|
||||
);
|
||||
},
|
||||
@@ -103,6 +102,30 @@ export const leftMenus = (clusterId?: string, clusterRunState?: number) => ({
|
||||
},
|
||||
].filter((m) => m),
|
||||
},
|
||||
{
|
||||
name: (intl: any) => {
|
||||
return (
|
||||
<div className="menu-item-with-beta-tag">
|
||||
<span>{intl.formatMessage({ id: 'menu.cluster.replication' })}</span>
|
||||
<div className="beta-tag"></div>
|
||||
</div>
|
||||
);
|
||||
},
|
||||
path: 'replication',
|
||||
icon: 'icon-Operation',
|
||||
children: [
|
||||
{
|
||||
name: (intl: any) => <span>{intl.formatMessage({ id: 'menu.cluster.replication.dashboard' })}</span>,
|
||||
path: '',
|
||||
icon: 'icon-luoji',
|
||||
},
|
||||
{
|
||||
name: (intl: any) => <span>{intl.formatMessage({ id: 'menu.cluster.replication.mirror-maker' })}</span>,
|
||||
path: 'mirror-maker',
|
||||
icon: '#icon-luoji',
|
||||
},
|
||||
].filter((m) => m),
|
||||
},
|
||||
{
|
||||
name: 'consumer-group',
|
||||
path: 'consumers',
|
||||
|
||||
@@ -52,6 +52,10 @@ export default {
|
||||
[`menu.${systemKey}.connect.connectors`]: 'Connectors',
|
||||
[`menu.${systemKey}.connect.workers`]: 'Workers',
|
||||
|
||||
[`menu.${systemKey}.replication`]: 'Replication',
|
||||
[`menu.${systemKey}.replication.dashboard`]: 'Overview',
|
||||
[`menu.${systemKey}.replication.mirror-maker`]: 'Mirror Makers',
|
||||
|
||||
[`menu.${systemKey}.acls`]: 'ACLs',
|
||||
|
||||
[`menu.${systemKey}.jobs`]: 'Job',
|
||||
|
||||
@@ -26,11 +26,19 @@ export enum ClustersPermissionMap {
|
||||
TOPIC_ADD = 'Topic-新增Topic',
|
||||
TOPIC_MOVE_REPLICA = 'Topic-迁移副本',
|
||||
TOPIC_CHANGE_REPLICA = 'Topic-扩缩副本',
|
||||
TOPIC_REPLICATOR = 'Topic-新增Topic复制',
|
||||
TOPIC_CANCEL_REPLICATOR = 'Topic-详情-取消Topic复制',
|
||||
// Consumers
|
||||
CONSUMERS_RESET_OFFSET = 'Consumers-重置Offset',
|
||||
// Test
|
||||
TEST_CONSUMER = 'Test-Consumer',
|
||||
TEST_PRODUCER = 'Test-Producer',
|
||||
// MM2
|
||||
MM2_ADD = 'MM2-新增',
|
||||
MM2_CHANGE_CONFIG = 'MM2-编辑',
|
||||
MM2_DELETE = 'MM2-删除',
|
||||
MM2_RESTART = 'MM2-重启',
|
||||
MM2_STOP_RESUME = 'MM2-暂停&恢复',
|
||||
}
|
||||
|
||||
export interface PermissionNode {
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,266 @@
|
||||
import api from '@src/api';
|
||||
import CodeMirrorFormItem from '@src/components/CodeMirrorFormItem';
|
||||
import customMessage from '@src/components/Message';
|
||||
import { Button, Divider, Drawer, Form, message, Space, Utils } from 'knowdesign';
|
||||
import React, { forwardRef, useEffect, useImperativeHandle, useState } from 'react';
|
||||
import { useParams } from 'react-router-dom';
|
||||
import { ConnectCluster, ConnectorPlugin, ConnectorPluginConfig, OperateInfo } from './AddMM2';
|
||||
|
||||
const PLACEHOLDER = `配置格式如下
|
||||
|
||||
{
|
||||
"connectClusterId": 1, // ConnectID
|
||||
"connectorName": "", // MM2 名称
|
||||
"sourceKafkaClusterId": 1, // SourceKafka集群 ID
|
||||
"configs": { // Source 相关配置
|
||||
"name": "", // MM2 名称
|
||||
"source.cluster.alias": "", // SourceKafka集群 ID
|
||||
...
|
||||
},
|
||||
"heartbeatConnectorConfigs": { // Heartbeat 相关配置
|
||||
"name": "", // Heartbeat 对应的Connector名称
|
||||
"source.cluster.alias": "", // SourceKafka集群 ID
|
||||
...
|
||||
},
|
||||
"checkpointConnectorConfigs": { // Checkpoint 相关配置
|
||||
"name": "", // Checkpoint 对应的Connector名称
|
||||
"source.cluster.alias": "", // SourceKafka集群 ID
|
||||
...
|
||||
}
|
||||
}`;
|
||||
|
||||
export default forwardRef((props: any, ref) => {
|
||||
// const { clusterId } = useParams<{
|
||||
// clusterId: string;
|
||||
// }>();
|
||||
const [visible, setVisible] = useState(false);
|
||||
const [form] = Form.useForm();
|
||||
const [type, setType] = useState('create');
|
||||
// const [connectClusters, setConnectClusters] = useState<{ label: string; value: number }[]>([]);
|
||||
const [defaultConfigs, setDefaultConfigs] = useState<{ [key: string]: any }>({});
|
||||
const [submitLoading, setSubmitLoading] = useState(false);
|
||||
|
||||
// const getConnectClusters = () => {
|
||||
// return Utils.request(api.getConnectClusters(clusterId)).then((res: ConnectCluster[]) => {
|
||||
// setConnectClusters(
|
||||
// res.map(({ name, id }) => ({
|
||||
// label: name || '-',
|
||||
// value: id,
|
||||
// }))
|
||||
// );
|
||||
// });
|
||||
// };
|
||||
|
||||
const onOpen = (type: 'create' | 'edit', defaultConfigs?: { [key: string]: any }) => {
|
||||
if (defaultConfigs) {
|
||||
setDefaultConfigs({ ...defaultConfigs });
|
||||
form.setFieldsValue({
|
||||
configs: JSON.stringify(defaultConfigs, null, 2),
|
||||
});
|
||||
}
|
||||
setType(type);
|
||||
setVisible(true);
|
||||
};
|
||||
|
||||
const onSubmit = () => {
|
||||
setSubmitLoading(true);
|
||||
form.validateFields().then(
|
||||
(data) => {
|
||||
const postData = JSON.parse(data.configs);
|
||||
|
||||
Object.entries(postData.configs).forEach(([key, val]) => {
|
||||
if (val === null) {
|
||||
delete postData.configs[key];
|
||||
}
|
||||
});
|
||||
Utils.put(api.validateMM2Config, postData).then(
|
||||
(res: ConnectorPluginConfig) => {
|
||||
if (res) {
|
||||
if (res?.errorCount > 0) {
|
||||
const errors: OperateInfo['errors'] = {};
|
||||
res?.configs
|
||||
?.filter((config) => config.value.errors.length !== 0)
|
||||
.forEach(({ value }) => {
|
||||
if (value.name.includes('transforms.')) {
|
||||
errors['transforms'] = (errors['transforms'] || []).concat(value.errors);
|
||||
} else {
|
||||
errors[value.name] = value.errors;
|
||||
}
|
||||
});
|
||||
form.setFields([
|
||||
{
|
||||
name: 'configs',
|
||||
errors: Object.entries(errors).map(([name, errorArr]) => `${name}: ${errorArr.join('; ')}\n`),
|
||||
},
|
||||
]);
|
||||
setSubmitLoading(false);
|
||||
} else {
|
||||
if (type === 'create') {
|
||||
Utils.post(api.mirrorMakerOperates, postData)
|
||||
.then(() => {
|
||||
customMessage.success('新建成功');
|
||||
onClose();
|
||||
props?.refresh();
|
||||
})
|
||||
.finally(() => setSubmitLoading(false));
|
||||
} else {
|
||||
Utils.put(api.updateMM2Config, postData)
|
||||
.then(() => {
|
||||
customMessage.success('编辑成功');
|
||||
props?.refresh();
|
||||
onClose();
|
||||
})
|
||||
.finally(() => setSubmitLoading(false));
|
||||
}
|
||||
}
|
||||
} else {
|
||||
setSubmitLoading(false);
|
||||
message.error('接口校验出错,请重新提交');
|
||||
}
|
||||
},
|
||||
() => setSubmitLoading(false)
|
||||
);
|
||||
},
|
||||
() => setSubmitLoading(false)
|
||||
);
|
||||
};
|
||||
|
||||
const onClose = () => {
|
||||
setVisible(false);
|
||||
form.resetFields();
|
||||
};
|
||||
|
||||
// useEffect(() => {
|
||||
// getConnectClusters();
|
||||
// }, []);
|
||||
|
||||
useImperativeHandle(ref, () => ({
|
||||
onOpen,
|
||||
onClose,
|
||||
}));
|
||||
|
||||
return (
|
||||
<Drawer
|
||||
title={`${type === 'create' ? '新建' : '编辑'} MM2`}
|
||||
className="operate-connector-drawer-use-json"
|
||||
width={800}
|
||||
visible={visible}
|
||||
onClose={onClose}
|
||||
maskClosable={false}
|
||||
extra={
|
||||
<div className="operate-wrap">
|
||||
<Space>
|
||||
<Button size="small" onClick={onClose}>
|
||||
取消
|
||||
</Button>
|
||||
<Button size="small" type="primary" onClick={onSubmit} loading={submitLoading}>
|
||||
确定
|
||||
</Button>
|
||||
<Divider type="vertical" />
|
||||
</Space>
|
||||
</div>
|
||||
}
|
||||
>
|
||||
<Form form={form} layout="vertical">
|
||||
<Form.Item
|
||||
name="configs"
|
||||
validateTrigger="onBlur"
|
||||
rules={[
|
||||
{
|
||||
validator(rule, value) {
|
||||
if (!value) {
|
||||
return Promise.reject('配置不能为空');
|
||||
}
|
||||
try {
|
||||
const v = JSON.parse(value);
|
||||
if (typeof v !== 'object') {
|
||||
return Promise.reject('输入内容必须为 JSON');
|
||||
}
|
||||
// let connectClusterId = -1;
|
||||
// ! 校验 connectorName 字段
|
||||
if (!v.connectorName) {
|
||||
return Promise.reject('内容缺少 MM2任务名称 字段或字段内容为空');
|
||||
} else {
|
||||
if (type === 'edit') {
|
||||
if (v.connectorName !== defaultConfigs.connectorName) {
|
||||
return Promise.reject('编辑模式下不允许修改 MM2任务名称 字段');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ! 校验connectClusterId
|
||||
if (!v.connectClusterId) {
|
||||
return Promise.reject('内容缺少 connectClusterId 字段或字段内容为空');
|
||||
}
|
||||
// ! 校验sourceKafkaClusterId
|
||||
if (!v.sourceKafkaClusterId) {
|
||||
return Promise.reject('内容缺少 sourceKafkaClusterId 字段或字段内容为空');
|
||||
}
|
||||
// ! 校验configs
|
||||
if (!v.configs || typeof v.configs !== 'object') {
|
||||
return Promise.reject('内容缺少 configs 字段或字段格式错误');
|
||||
} else {
|
||||
// ! 校验Topic
|
||||
if (!v.configs.topics) {
|
||||
return Promise.reject('configs 字段下缺少 topics 项');
|
||||
}
|
||||
|
||||
// 校验 connectorName 字段
|
||||
// if (!v.configs.name) {
|
||||
// return Promise.reject('configs 字段下缺少 name 项');
|
||||
// } else {
|
||||
// if (type === 'edit' && v.configs.name !== defaultConfigs.name) {
|
||||
// return Promise.reject('编辑模式下不允许修改 name 字段');
|
||||
// }
|
||||
// }
|
||||
// if (!v.configs['connector.class']) {
|
||||
// return Promise.reject('configs 字段下缺少 connector.class 项');
|
||||
// } else if (type === 'edit' && v.configs['connector.class'] !== defaultConfigs['connector.class']) {
|
||||
// return Promise.reject('编辑模式下不允许修改 connector.class 字段');
|
||||
// }
|
||||
}
|
||||
return Promise.resolve();
|
||||
// if (type === 'create') {
|
||||
// // 异步校验 connector 名称是否重复 以及 className 是否存在
|
||||
// return Promise.all([
|
||||
// Utils.request(api.isConnectorExist(connectClusterId, v.configs.name)),
|
||||
// Utils.request(api.getConnectorPlugins(connectClusterId)),
|
||||
// ]).then(
|
||||
// ([data, plugins]: [any, ConnectorPlugin[]]) => {
|
||||
// return data?.exist
|
||||
// ? Promise.reject('name 与已有 Connector 重复')
|
||||
// : plugins.every((plugin) => plugin.className !== v.configs['connector.class'])
|
||||
// ? Promise.reject('该 connectCluster 下不存在 connector.class 项配置的插件')
|
||||
// : Promise.resolve();
|
||||
// },
|
||||
// () => {
|
||||
// return Promise.reject('接口校验出错,请重试');
|
||||
// }
|
||||
// );
|
||||
// } else {
|
||||
// return Promise.resolve();
|
||||
// }
|
||||
} catch (e) {
|
||||
return Promise.reject('输入内容必须为 JSON');
|
||||
}
|
||||
},
|
||||
},
|
||||
]}
|
||||
>
|
||||
{visible && (
|
||||
<div>
|
||||
<CodeMirrorFormItem
|
||||
resize
|
||||
defaultInput={form.getFieldValue('configs')}
|
||||
placeholder={PLACEHOLDER}
|
||||
onBeforeChange={(configs: string) => {
|
||||
form.setFieldsValue({ configs });
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
</Form.Item>
|
||||
</Form>
|
||||
</Drawer>
|
||||
);
|
||||
});
|
||||
@@ -0,0 +1,99 @@
|
||||
import React, { useState } from 'react';
|
||||
import { useParams } from 'react-router-dom';
|
||||
import { Button, Form, Input, Modal, Utils } from 'knowdesign';
|
||||
import notification from '@src/components/Notification';
|
||||
import { IconFont } from '@knowdesign/icons';
|
||||
import Api from '@src/api/index';
|
||||
|
||||
// eslint-disable-next-line react/display-name
|
||||
const DeleteConnector = (props: { record: any; onConfirm?: () => void }) => {
|
||||
const { record, onConfirm } = props;
|
||||
const [form] = Form.useForm();
|
||||
const [delDialogVisible, setDelDialogVisble] = useState(false);
|
||||
const handleDelOk = () => {
|
||||
form.validateFields().then((e) => {
|
||||
const formVal = form.getFieldsValue();
|
||||
formVal.connectClusterId = Number(record.connectClusterId);
|
||||
Utils.delete(Api.mirrorMakerOperates, { data: formVal }).then((res: any) => {
|
||||
if (res === null) {
|
||||
notification.success({
|
||||
message: '删除成功',
|
||||
});
|
||||
setDelDialogVisble(false);
|
||||
onConfirm && onConfirm();
|
||||
} else {
|
||||
notification.error({
|
||||
message: '删除失败',
|
||||
});
|
||||
}
|
||||
});
|
||||
});
|
||||
};
|
||||
return (
|
||||
<>
|
||||
<Button
|
||||
type="link"
|
||||
size="small"
|
||||
onClick={(_) => {
|
||||
setDelDialogVisble(true);
|
||||
}}
|
||||
>
|
||||
删除
|
||||
</Button>
|
||||
<Modal
|
||||
className="custom-modal"
|
||||
title="确定删除此 MM2 任务吗?"
|
||||
centered={true}
|
||||
visible={delDialogVisible}
|
||||
wrapClassName="del-connect-modal"
|
||||
destroyOnClose={true}
|
||||
maskClosable={false}
|
||||
onOk={handleDelOk}
|
||||
onCancel={(_) => {
|
||||
setDelDialogVisble(false);
|
||||
}}
|
||||
okText="删除"
|
||||
okButtonProps={{
|
||||
danger: true,
|
||||
size: 'small',
|
||||
style: {
|
||||
paddingLeft: '16px',
|
||||
paddingRight: '16px',
|
||||
},
|
||||
}}
|
||||
cancelButtonProps={{
|
||||
size: 'small',
|
||||
style: {
|
||||
paddingLeft: '16px',
|
||||
paddingRight: '16px',
|
||||
},
|
||||
}}
|
||||
>
|
||||
<Form form={form} labelCol={{ span: 6 }} wrapperCol={{ span: 16 }} style={{ marginTop: 17 }}>
|
||||
<Form.Item label="MM2 Name">{record.connectorName}</Form.Item>
|
||||
<Form.Item
|
||||
name="connectorName"
|
||||
label="MM2 Name"
|
||||
rules={[
|
||||
// { required: true },
|
||||
() => ({
|
||||
validator(_, value) {
|
||||
if (!value) {
|
||||
return Promise.reject(new Error('请输入MM2 Name名称'));
|
||||
} else if (value !== record.connectorName) {
|
||||
return Promise.reject(new Error('请输入正确的MM2 Name名称'));
|
||||
}
|
||||
return Promise.resolve();
|
||||
},
|
||||
}),
|
||||
]}
|
||||
>
|
||||
<Input placeholder="请输入" size="small"></Input>
|
||||
</Form.Item>
|
||||
</Form>
|
||||
</Modal>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default DeleteConnector;
|
||||
@@ -0,0 +1,185 @@
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import { Drawer, Utils, AppContainer, ProTable, Tabs, Empty, Spin } from 'knowdesign';
|
||||
import API from '@src/api';
|
||||
import MirrorMakerDetailCard from '@src/components/CardBar/MirrorMakerDetailCard';
|
||||
import { defaultPagination, getMM2DetailColumns } from './config';
|
||||
import notification from '@src/components/Notification';
|
||||
import './index.less';
|
||||
const { TabPane } = Tabs;
|
||||
const prefix = 'mm2-detail';
|
||||
const { request } = Utils;
|
||||
|
||||
const DetailTable = ({ loading, retryOption, data }: { loading: boolean; retryOption: any; data: any[] }) => {
|
||||
const [pagination, setPagination] = useState<any>(defaultPagination);
|
||||
const onTableChange = (pagination: any, filters: any, sorter: any) => {
|
||||
setPagination(pagination);
|
||||
};
|
||||
return (
|
||||
<Spin spinning={loading}>
|
||||
{data.length ? (
|
||||
<ProTable
|
||||
key="mm2-detail-table"
|
||||
showQueryForm={false}
|
||||
tableProps={{
|
||||
showHeader: false,
|
||||
rowKey: 'taskId',
|
||||
// loading: loading,
|
||||
columns: getMM2DetailColumns({ retryOption }),
|
||||
dataSource: data,
|
||||
paginationProps: { ...pagination },
|
||||
attrs: {
|
||||
onChange: onTableChange,
|
||||
// scroll: { x: 'max-content' },
|
||||
bordered: false,
|
||||
},
|
||||
}}
|
||||
/>
|
||||
) : (
|
||||
<Empty description="暂无数据" image={Empty.PRESENTED_IMAGE_CUSTOM} style={{ padding: '100px 0' }} />
|
||||
)}
|
||||
</Spin>
|
||||
);
|
||||
};
|
||||
|
||||
const MM2Detail = (props: any) => {
|
||||
const { visible, setVisible, record } = props;
|
||||
const [global] = AppContainer.useGlobalValue();
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [data, setData] = useState([]);
|
||||
|
||||
const [tabSelectType, setTabSelectType] = useState<string>('MirrorSource');
|
||||
const onClose = () => {
|
||||
setVisible(false);
|
||||
setTabSelectType('MirrorSource');
|
||||
// setPagination(defaultPagination);
|
||||
// clean hash
|
||||
};
|
||||
const callback = (key: any) => {
|
||||
setTabSelectType(key);
|
||||
};
|
||||
|
||||
const genData: any = {
|
||||
MirrorSource: async () => {
|
||||
if (global?.clusterInfo?.id === undefined) return;
|
||||
setData([]);
|
||||
setLoading(true);
|
||||
if (record.connectorName) {
|
||||
request(API.getConnectDetailTasks(record.connectorName, record.connectClusterId))
|
||||
.then((res: any) => {
|
||||
setData(res || []);
|
||||
setLoading(false);
|
||||
})
|
||||
.catch((err) => {
|
||||
setLoading(false);
|
||||
});
|
||||
} else {
|
||||
setLoading(false);
|
||||
}
|
||||
},
|
||||
MirrorCheckpoint: async () => {
|
||||
if (global?.clusterInfo?.id === undefined) return;
|
||||
setData([]);
|
||||
setLoading(true);
|
||||
if (record.checkpointConnector) {
|
||||
request(API.getConnectDetailTasks(record.checkpointConnector, record.connectClusterId))
|
||||
.then((res: any) => {
|
||||
setData(res || []);
|
||||
setLoading(false);
|
||||
})
|
||||
.catch((err) => {
|
||||
setLoading(false);
|
||||
});
|
||||
} else {
|
||||
setLoading(false);
|
||||
}
|
||||
},
|
||||
MirrorHeatbeat: async () => {
|
||||
if (global?.clusterInfo?.id === undefined) return;
|
||||
setData([]);
|
||||
setLoading(true);
|
||||
if (record.heartbeatConnector) {
|
||||
request(API.getConnectDetailTasks(record.heartbeatConnector, record.connectClusterId))
|
||||
.then((res: any) => {
|
||||
setData(res || []);
|
||||
setLoading(false);
|
||||
})
|
||||
.catch((err) => {
|
||||
setLoading(false);
|
||||
});
|
||||
} else {
|
||||
setLoading(false);
|
||||
}
|
||||
},
|
||||
};
|
||||
|
||||
const retryOption = (taskId: any) => {
|
||||
const params = {
|
||||
action: 'restart',
|
||||
connectClusterId: record?.connectClusterId,
|
||||
connectorName: record?.connectorName,
|
||||
taskId,
|
||||
};
|
||||
// 需要区分 tabSelectType
|
||||
request(API.optionTasks(), { method: 'PUT', data: params }).then((res: any) => {
|
||||
if (res === null) {
|
||||
notification.success({
|
||||
message: `任务重试成功`,
|
||||
});
|
||||
genData[tabSelectType]();
|
||||
} else {
|
||||
notification.error({
|
||||
message: `任务重试失败`,
|
||||
});
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
visible && record && genData[tabSelectType]();
|
||||
}, [visible, tabSelectType]);
|
||||
|
||||
return (
|
||||
<Drawer
|
||||
// push={false}
|
||||
title={
|
||||
<span>
|
||||
<span style={{ fontSize: '18px', fontFamily: 'PingFangSC-Semibold', color: '#495057' }}>{record.connectorName ?? '-'}</span>
|
||||
</span>
|
||||
}
|
||||
width={1080}
|
||||
placement="right"
|
||||
onClose={onClose}
|
||||
visible={visible}
|
||||
className={`${prefix}-drawer`}
|
||||
destroyOnClose
|
||||
maskClosable={false}
|
||||
>
|
||||
<MirrorMakerDetailCard record={record} tabSelectType={tabSelectType} />
|
||||
<Tabs
|
||||
className={'custom_tabs_class'}
|
||||
defaultActiveKey="Configuration"
|
||||
// activeKey={tabSelectType}
|
||||
onChange={callback}
|
||||
destroyInactiveTabPane
|
||||
>
|
||||
<TabPane tab="MirrorSource" key="MirrorSource">
|
||||
<DetailTable loading={loading} retryOption={retryOption} data={data} />
|
||||
{/* {global.isShowControl && global.isShowControl(ControlStatusMap.BROKER_DETAIL_CONFIG) ? (
|
||||
<Configuration searchKeywords={searchKeywords} tabSelectType={tabSelectType} hashData={hashData} />
|
||||
) : (
|
||||
<Empty description="当前版本过低,不支持该功能!" />
|
||||
)} */}
|
||||
</TabPane>
|
||||
<TabPane tab="MirrorCheckpoint" key="MirrorCheckpoint">
|
||||
<DetailTable loading={loading} retryOption={retryOption} data={data} />
|
||||
</TabPane>
|
||||
<TabPane tab="MirrorHeatbeat" key="MirrorHeatbeat">
|
||||
<DetailTable loading={loading} retryOption={retryOption} data={data} />
|
||||
</TabPane>
|
||||
</Tabs>
|
||||
{/* <BrokerDetailHealthCheck record={{ brokerId: hashData?.brokerId }} /> */}
|
||||
</Drawer>
|
||||
);
|
||||
};
|
||||
|
||||
export default MM2Detail;
|
||||
@@ -0,0 +1,344 @@
|
||||
import SmallChart from '@src/components/SmallChart';
|
||||
import { IconFont } from '@knowdesign/icons';
|
||||
import { Button, Tag, Tooltip, Utils, Popconfirm, AppContainer } from 'knowdesign';
|
||||
import React from 'react';
|
||||
import Delete from './Delete';
|
||||
import { ClustersPermissionMap } from '../CommonConfig';
|
||||
export const defaultPagination = {
|
||||
current: 1,
|
||||
pageSize: 10,
|
||||
position: 'bottomRight',
|
||||
showSizeChanger: true,
|
||||
pageSizeOptions: ['10', '20', '50', '100', '200', '500'],
|
||||
};
|
||||
|
||||
export const optionType: { [name: string]: string } = {
|
||||
['stop']: '暂停',
|
||||
['restart']: '重启',
|
||||
['resume']: '继续',
|
||||
};
|
||||
|
||||
export const stateEnum: any = {
|
||||
['UNASSIGNED']: {
|
||||
// 未分配
|
||||
name: 'Unassigned',
|
||||
color: '#556EE6',
|
||||
bgColor: '#EBEEFA',
|
||||
},
|
||||
['RUNNING']: {
|
||||
// 运行
|
||||
name: 'Running',
|
||||
color: '#00C0A2',
|
||||
bgColor: 'rgba(0,192,162,0.10)',
|
||||
},
|
||||
['PAUSED']: {
|
||||
// 暂停
|
||||
name: 'Paused',
|
||||
color: '#495057',
|
||||
bgColor: '#ECECF6',
|
||||
},
|
||||
['FAILED']: {
|
||||
// 失败
|
||||
name: 'Failed',
|
||||
color: '#F58342',
|
||||
bgColor: '#fef3e5',
|
||||
},
|
||||
['DESTROYED']: {
|
||||
// 销毁
|
||||
name: 'Destroyed',
|
||||
color: '#FF7066',
|
||||
bgColor: '#fdefee',
|
||||
},
|
||||
['RESTARTING']: {
|
||||
// 重新启动
|
||||
name: 'Restarting',
|
||||
color: '#3991FF',
|
||||
bgColor: '#e9f5ff',
|
||||
},
|
||||
};
|
||||
|
||||
const calcCurValue = (record: any, metricName: string) => {
|
||||
// const item = (record.metricPoints || []).find((item: any) => item.metricName === metricName);
|
||||
// return item?.value || '';
|
||||
// TODO 替换record
|
||||
const orgVal = record?.latestMetrics?.metrics?.[metricName];
|
||||
if (orgVal !== undefined) {
|
||||
if (metricName === 'TotalRecordErrors') {
|
||||
return Math.round(orgVal).toLocaleString();
|
||||
} else {
|
||||
return Number(Utils.formatAssignSize(orgVal, 'KB', orgVal > 1000 ? 2 : 3)).toLocaleString();
|
||||
// return Utils.formatAssignSize(orgVal, 'KB');
|
||||
}
|
||||
}
|
||||
return '-';
|
||||
// return orgVal !== undefined ? (metricName !== 'HealthScore' ? formatAssignSize(orgVal, 'KB') : orgVal) : '-';
|
||||
};
|
||||
|
||||
const renderLine = (record: any, metricName: string) => {
|
||||
const points = record.metricLines?.find((item: any) => item.metricName === metricName)?.metricPoints || [];
|
||||
return points.length ? (
|
||||
<div className="metric-data-wrap">
|
||||
<SmallChart
|
||||
width={'100%'}
|
||||
height={30}
|
||||
chartData={{
|
||||
name: record.metricName,
|
||||
data: points.map((item: any) => ({ time: item.timeStamp, value: item.value })),
|
||||
}}
|
||||
/>
|
||||
<span className="cur-val">{calcCurValue(record, metricName)}</span>
|
||||
</div>
|
||||
) : (
|
||||
<span className="cur-val">{calcCurValue(record, metricName)}</span>
|
||||
);
|
||||
};
|
||||
|
||||
export const getMM2Columns = (arg?: any) => {
|
||||
// eslint-disable-next-line react-hooks/rules-of-hooks
|
||||
const [global] = AppContainer.useGlobalValue();
|
||||
const columns: any = [
|
||||
{
|
||||
title: 'MM2 Name',
|
||||
dataIndex: 'connectorName',
|
||||
key: 'connectorName',
|
||||
width: 160,
|
||||
fixed: 'left',
|
||||
lineClampOne: true,
|
||||
render: (t: string, r: any) => {
|
||||
return t ? (
|
||||
<>
|
||||
<Tooltip placement="bottom" title={t}>
|
||||
<a
|
||||
onClick={() => {
|
||||
arg.getDetailInfo(r);
|
||||
}}
|
||||
>
|
||||
{t}
|
||||
</a>
|
||||
</Tooltip>
|
||||
</>
|
||||
) : (
|
||||
'-'
|
||||
);
|
||||
},
|
||||
},
|
||||
{
|
||||
title: 'Connect集群',
|
||||
dataIndex: 'connectClusterName',
|
||||
key: 'connectClusterName',
|
||||
width: 200,
|
||||
lineClampOne: true,
|
||||
needTooltip: true,
|
||||
},
|
||||
{
|
||||
title: 'State',
|
||||
dataIndex: 'state',
|
||||
key: 'state',
|
||||
width: 120,
|
||||
render: (t: string, r: any) => {
|
||||
return t ? (
|
||||
<Tag
|
||||
style={{
|
||||
background: stateEnum[t]?.bgColor,
|
||||
color: stateEnum[t]?.color,
|
||||
padding: '3px 6px',
|
||||
}}
|
||||
>
|
||||
{stateEnum[t]?.name}
|
||||
</Tag>
|
||||
) : (
|
||||
'-'
|
||||
);
|
||||
},
|
||||
},
|
||||
{
|
||||
// title: '集群(源-->目标)',
|
||||
title: (
|
||||
<span>
|
||||
集群(源{' '}
|
||||
<span>
|
||||
<IconFont type="icon-jiantou" />
|
||||
</span>{' '}
|
||||
目标)
|
||||
</span>
|
||||
),
|
||||
dataIndex: 'destKafkaClusterName',
|
||||
key: 'destKafkaClusterName',
|
||||
width: 200,
|
||||
render: (t: string, r: any) => {
|
||||
return r.sourceKafkaClusterName && r.destKafkaClusterName ? (
|
||||
<span>
|
||||
<span>{r.sourceKafkaClusterName} </span>
|
||||
<IconFont type="icon-jiantou" />
|
||||
<span> {t}</span>
|
||||
</span>
|
||||
) : (
|
||||
'-'
|
||||
);
|
||||
},
|
||||
},
|
||||
{
|
||||
title: 'Tasks',
|
||||
dataIndex: 'taskCount',
|
||||
key: 'taskCount',
|
||||
width: 100,
|
||||
},
|
||||
{
|
||||
title: '复制流量速率',
|
||||
dataIndex: 'byteRate',
|
||||
key: 'byteRate',
|
||||
sorter: true,
|
||||
width: 170,
|
||||
render: (value: any, record: any) => renderLine(record, 'ByteRate'),
|
||||
},
|
||||
{
|
||||
title: '消息复制速率',
|
||||
dataIndex: 'recordRate',
|
||||
key: 'recordRate',
|
||||
sorter: true,
|
||||
width: 170,
|
||||
render: (value: any, record: any) => renderLine(record, 'RecordRate'),
|
||||
},
|
||||
{
|
||||
title: '最大延迟',
|
||||
dataIndex: 'replicationLatencyMsMax',
|
||||
key: 'replicationLatencyMsMax',
|
||||
sorter: true,
|
||||
width: 170,
|
||||
render: (value: any, record: any) => renderLine(record, 'ReplicationLatencyMsMax'),
|
||||
},
|
||||
];
|
||||
if (global.hasPermission) {
|
||||
columns.push({
|
||||
title: '操作',
|
||||
dataIndex: 'options',
|
||||
key: 'options',
|
||||
width: 200,
|
||||
filterTitle: true,
|
||||
fixed: 'right',
|
||||
// eslint-disable-next-line react/display-name
|
||||
render: (_t: any, r: any) => {
|
||||
return (
|
||||
<div>
|
||||
{global.hasPermission(ClustersPermissionMap.MM2_STOP_RESUME) && (r.state === 'RUNNING' || r.state === 'PAUSED') && (
|
||||
<Popconfirm
|
||||
title={`是否${r.state === 'RUNNING' ? '暂停' : '继续'}当前任务?`}
|
||||
onConfirm={() => arg?.optionConnect(r, r.state === 'RUNNING' ? 'stop' : 'resume')}
|
||||
// onCancel={cancel}
|
||||
okText="是"
|
||||
cancelText="否"
|
||||
overlayClassName="connect-popconfirm"
|
||||
>
|
||||
<Button key="stopResume" type="link" size="small">
|
||||
{/* {r?.state !== 1 ? '继续' : '暂停'} */}
|
||||
{r.state === 'RUNNING' ? '暂停' : '继续'}
|
||||
</Button>
|
||||
</Popconfirm>
|
||||
)}
|
||||
{global.hasPermission(ClustersPermissionMap.MM2_RESTART) ? (
|
||||
<Popconfirm
|
||||
title="是否重启当前任务?"
|
||||
onConfirm={() => arg?.optionConnect(r, 'restart')}
|
||||
// onCancel={cancel}
|
||||
okText="是"
|
||||
cancelText="否"
|
||||
overlayClassName="connect-popconfirm"
|
||||
>
|
||||
<Button key="restart" type="link" size="small">
|
||||
重启
|
||||
</Button>
|
||||
</Popconfirm>
|
||||
) : (
|
||||
<></>
|
||||
)}
|
||||
{global.hasPermission(ClustersPermissionMap.MM2_CHANGE_CONFIG) ? (
|
||||
r.sourceKafkaClusterId ? (
|
||||
<Button type="link" size="small" onClick={() => arg?.editConnector(r)}>
|
||||
编辑
|
||||
</Button>
|
||||
) : (
|
||||
<Tooltip title="非本平台创建的任务无法编辑">
|
||||
<Button type="link" disabled size="small">
|
||||
编辑
|
||||
</Button>
|
||||
</Tooltip>
|
||||
)
|
||||
) : (
|
||||
<></>
|
||||
)}
|
||||
{global.hasPermission(ClustersPermissionMap.MM2_DELETE) ? <Delete record={r} onConfirm={arg?.deleteTesk}></Delete> : <></>}
|
||||
</div>
|
||||
);
|
||||
},
|
||||
});
|
||||
}
|
||||
return columns;
|
||||
};
|
||||
|
||||
// Detail
|
||||
export const getMM2DetailColumns = (arg?: any) => {
|
||||
const columns = [
|
||||
{
|
||||
title: 'Task ID',
|
||||
dataIndex: 'taskId',
|
||||
key: 'taskId',
|
||||
width: 240,
|
||||
render: (t: any, r: any) => {
|
||||
return (
|
||||
<span>
|
||||
{t}
|
||||
{
|
||||
<Tag
|
||||
style={{
|
||||
background: stateEnum[r?.state]?.bgColor,
|
||||
color: stateEnum[r?.state]?.color,
|
||||
padding: '3px 6px',
|
||||
marginLeft: '5px',
|
||||
}}
|
||||
>
|
||||
{Utils.firstCharUppercase(r?.state as string)}
|
||||
</Tag>
|
||||
}
|
||||
</span>
|
||||
);
|
||||
},
|
||||
},
|
||||
{
|
||||
title: 'Worker',
|
||||
dataIndex: 'workerId',
|
||||
key: 'workerId',
|
||||
width: 240,
|
||||
},
|
||||
{
|
||||
title: '错误原因',
|
||||
dataIndex: 'trace',
|
||||
key: 'trace',
|
||||
width: 400,
|
||||
needTooltip: true,
|
||||
lineClampOne: true,
|
||||
},
|
||||
{
|
||||
title: '操作',
|
||||
dataIndex: 'role',
|
||||
key: 'role',
|
||||
width: 100,
|
||||
render: (_t: any, r: any) => {
|
||||
return (
|
||||
<div>
|
||||
<Popconfirm
|
||||
title="是否重试当前任务?"
|
||||
onConfirm={() => arg?.retryOption(r.taskId)}
|
||||
// onCancel={cancel}
|
||||
okText="是"
|
||||
cancelText="否"
|
||||
overlayClassName="connect-popconfirm"
|
||||
>
|
||||
<a>重试</a>
|
||||
</Popconfirm>
|
||||
</div>
|
||||
);
|
||||
},
|
||||
},
|
||||
];
|
||||
return columns;
|
||||
};
|
||||
@@ -0,0 +1,265 @@
|
||||
// mm2列表 图表
|
||||
.metric-data-wrap {
|
||||
// display: flex;
|
||||
// align-items: center;
|
||||
width: 100%;
|
||||
.cur-val {
|
||||
display: block;
|
||||
text-align: right;
|
||||
}
|
||||
.dcloud-spin-nested-loading {
|
||||
flex: 1;
|
||||
}
|
||||
}
|
||||
|
||||
// 新增按钮
|
||||
.add-connect {
|
||||
.dcloud-btn-primary:hover,
|
||||
.dcloud-btn-primary:focus {
|
||||
// 可以控制新增按钮的hover和focus的样式
|
||||
// background: #556ee6;
|
||||
// border-color: #556ee6;
|
||||
}
|
||||
&-btn {
|
||||
border-top-right-radius: 0 !important;
|
||||
border-bottom-right-radius: 0 !important;
|
||||
border-right: none;
|
||||
}
|
||||
&-dropdown-menu {
|
||||
.dcloud-dropdown-menu {
|
||||
border-radius: 8px;
|
||||
&-item {
|
||||
font-size: 13px;
|
||||
}
|
||||
}
|
||||
}
|
||||
&-json {
|
||||
padding: 8px 12px !important;
|
||||
border-top-left-radius: 0 !important;
|
||||
border-bottom-left-radius: 0 !important;
|
||||
border-left: none;
|
||||
}
|
||||
}
|
||||
|
||||
// connect详情
|
||||
.mm2-detail-drawer {
|
||||
.card-bar-container {
|
||||
background: rgba(86, 110, 230, 0.04) !important;
|
||||
|
||||
.card-bar-colunms {
|
||||
background-color: rgba(86, 110, 230, 0);
|
||||
}
|
||||
}
|
||||
&-title {
|
||||
margin: 20px 0 8px;
|
||||
font-family: @font-family-bold;
|
||||
font-size: 16px;
|
||||
font-weight: 500;
|
||||
}
|
||||
}
|
||||
|
||||
// 删除
|
||||
.del-connect-modal {
|
||||
.tip-info {
|
||||
display: flex;
|
||||
color: #592d00;
|
||||
padding: 6px 14px;
|
||||
font-size: 13px;
|
||||
background: #fffae0;
|
||||
border-radius: 4px;
|
||||
.anticon {
|
||||
color: #ffc300;
|
||||
margin-right: 4px;
|
||||
margin-top: 3px;
|
||||
}
|
||||
.test-right-away {
|
||||
color: #556ee6;
|
||||
cursor: pointer;
|
||||
}
|
||||
.dcloud-alert-content {
|
||||
flex: none;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 重启、继续/暂停 气泡卡片
|
||||
.connect-popconfirm {
|
||||
.dcloud-popover-inner-content {
|
||||
padding: 6px 16px;
|
||||
}
|
||||
.dcloud-popover-inner {
|
||||
border-radius: 8px;
|
||||
}
|
||||
.dcloud-popover-message,
|
||||
.dcloud-btn {
|
||||
font-size: 12px !important;
|
||||
}
|
||||
}
|
||||
|
||||
.operate-connector-drawer {
|
||||
.connector-plugin-desc {
|
||||
font-size: 13px;
|
||||
.connector-plugin-title {
|
||||
font-family: @font-family-bold;
|
||||
}
|
||||
}
|
||||
.dcloud-collapse.add-connector-collapse {
|
||||
.add-connector-collapse-panel,
|
||||
.add-connector-collapse-panel:last-child {
|
||||
margin-bottom: 8px;
|
||||
overflow: hidden;
|
||||
background: #f8f9fa;
|
||||
border: 0px;
|
||||
border-radius: 8px;
|
||||
.dcloud-collapse-header {
|
||||
padding: 8px 12px;
|
||||
font-size: 14px;
|
||||
color: #495057;
|
||||
.dcloud-collapse-arrow {
|
||||
margin-right: 8px !important;
|
||||
font-size: 10px;
|
||||
}
|
||||
}
|
||||
&:hover .dcloud-collapse-extra {
|
||||
opacity: 1;
|
||||
}
|
||||
&:not(.dcloud-collapse-item-active) {
|
||||
.dcloud-collapse-header:hover {
|
||||
background: #f1f3ff;
|
||||
}
|
||||
}
|
||||
.dcloud-collapse-content-box {
|
||||
display: flex;
|
||||
flex-flow: row wrap;
|
||||
justify-content: space-between;
|
||||
padding: 20px 14px 0 14px;
|
||||
.dcloud-form-item {
|
||||
flex: 1 0 50%;
|
||||
.dcloud-input-number {
|
||||
width: 100%;
|
||||
}
|
||||
&:nth-child(2n) {
|
||||
padding-left: 6px;
|
||||
}
|
||||
&:nth-child(2n + 1) {
|
||||
padding-right: 6px;
|
||||
}
|
||||
.dcloud-form-item-control-input {
|
||||
height: 100%;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.add-container-plugin-select {
|
||||
height: 27px;
|
||||
margin: 4px 0;
|
||||
position: relative;
|
||||
.dcloud-form-item {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
.dcloud-form-item-control-input {
|
||||
min-height: 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.dcloud-alert {
|
||||
margin: 16px 0 24px 0;
|
||||
padding: 0 12px;
|
||||
border: unset;
|
||||
border-radius: 8px;
|
||||
background: #fffae0;
|
||||
.dcloud-alert-message {
|
||||
font-size: 13px;
|
||||
color: #592d00;
|
||||
.dcloud-btn {
|
||||
padding: 0 4px;
|
||||
font-size: 13px;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.operate-connector-drawer-use-json {
|
||||
.CodeMirror.cm-s-default {
|
||||
height: calc(100vh - 146px);
|
||||
}
|
||||
.dcloud-form-item {
|
||||
margin-bottom: 0 !important;
|
||||
}
|
||||
}
|
||||
|
||||
.mirror-maker-steps {
|
||||
width: 340px !important;
|
||||
margin: 0 auto !important;
|
||||
}
|
||||
|
||||
.add-mm2-config-title {
|
||||
color: #556ee6;
|
||||
margin-bottom: 24px;
|
||||
display: inline-block;
|
||||
cursor: pointer;
|
||||
&-text {
|
||||
margin-right: 7px;
|
||||
}
|
||||
&-icon {
|
||||
transform: rotate(180deg);
|
||||
}
|
||||
}
|
||||
|
||||
.custom-form-item-27 {
|
||||
padding: 0 16px;
|
||||
.dcloud-form-item {
|
||||
margin-bottom: 6px !important;
|
||||
}
|
||||
.dcloud-form-item-label > label {
|
||||
height: 27px;
|
||||
}
|
||||
.group-offset-table {
|
||||
margin-bottom: 40px;
|
||||
margin-top: 12px;
|
||||
}
|
||||
}
|
||||
|
||||
.custom-form-item-36 {
|
||||
.dcloud-form-item-control-input {
|
||||
min-height: 36px;
|
||||
}
|
||||
}
|
||||
|
||||
.clear-topic-minHeight {
|
||||
.dcloud-form-item-control-input {
|
||||
min-height: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.add-mm2-flex-layout {
|
||||
&-item {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
}
|
||||
.senior-config-left {
|
||||
width: 228px !important;
|
||||
margin-right: 10px;
|
||||
// .dcloud-form-item-label {
|
||||
// width: 190px !important;
|
||||
// text-align: right !important;
|
||||
// }
|
||||
}
|
||||
.dcloud-form-item {
|
||||
width: 345px;
|
||||
flex-direction: row !important;
|
||||
height: 27px;
|
||||
margin-bottom: 2px !important;
|
||||
&-label {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: flex-end;
|
||||
padding: 0 !important;
|
||||
margin-right: 10px;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,240 @@
|
||||
import React, { useState, useEffect, useRef } from 'react';
|
||||
import { ProTable, Dropdown, Button, Utils, AppContainer, SearchInput, Menu } from 'knowdesign';
|
||||
import { IconFont } from '@knowdesign/icons';
|
||||
import API from '../../api';
|
||||
import { getMM2Columns, defaultPagination, optionType } from './config';
|
||||
import { tableHeaderPrefix } from '@src/constants/common';
|
||||
import MirrorMakerCard from '@src/components/CardBar/MirrorMakerCard';
|
||||
import DBreadcrumb from 'knowdesign/es/extend/d-breadcrumb';
|
||||
import AddMM2, { OperateInfo } from './AddMM2';
|
||||
import MM2Detail from './Detail';
|
||||
import notification from '@src/components/Notification';
|
||||
import './index.less';
|
||||
import AddConnectorUseJSON from './AddMM2JSON';
|
||||
import { ClustersPermissionMap } from '../CommonConfig';
|
||||
const { request } = Utils;
|
||||
|
||||
const rateMap: any = {
|
||||
byteRate: ['ByteRate'],
|
||||
recordRate: ['RecordRate'],
|
||||
replicationLatencyMsMax: ['ReplicationLatencyMsMax'],
|
||||
};
|
||||
|
||||
const MirrorMaker2: React.FC = () => {
|
||||
const [global] = AppContainer.useGlobalValue();
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [detailVisible, setDetailVisible] = useState(false);
|
||||
const [data, setData] = useState([]);
|
||||
const [searchKeywords, setSearchKeywords] = useState('');
|
||||
const [pagination, setPagination] = useState<any>(defaultPagination);
|
||||
const [sortInfo, setSortInfo] = useState({});
|
||||
const [detailRecord, setDetailRecord] = useState('');
|
||||
const [healthType, setHealthType] = useState(true);
|
||||
const addConnectorRef = useRef(null);
|
||||
const addConnectorJsonRef = useRef(null);
|
||||
|
||||
const getRecent1DayTimeStamp = () => [Date.now() - 24 * 60 * 60 * 1000, Date.now()];
|
||||
// 请求接口获取数据
|
||||
const genData = async ({ pageNo, pageSize, filters, sorter }: any) => {
|
||||
const [startStamp, endStamp] = getRecent1DayTimeStamp();
|
||||
if (global?.clusterInfo?.id === undefined) return;
|
||||
setLoading(true);
|
||||
const params = {
|
||||
metricLines: {
|
||||
aggType: 'avg',
|
||||
endTime: endStamp,
|
||||
metricsNames: ['ByteRate', 'RecordRate', 'ReplicationLatencyMsMax'],
|
||||
// metricsNames: ['SourceRecordPollRate', 'SourceRecordWriteRate', 'SinkRecordReadRate', 'SinkRecordSendRate', 'TotalRecordErrors'],
|
||||
startTime: startStamp,
|
||||
topNu: 0,
|
||||
},
|
||||
searchKeywords: searchKeywords.slice(0, 128),
|
||||
pageNo,
|
||||
pageSize,
|
||||
latestMetricNames: ['ByteRate', 'RecordRate', 'ReplicationLatencyMsMax'],
|
||||
// latestMetricNames: ['SourceRecordPollRate', 'SourceRecordWriteRate', 'SinkRecordReadRate', 'SinkRecordSendRate', 'TotalRecordErrors'],
|
||||
sortType: sorter?.order ? sorter.order.substring(0, sorter.order.indexOf('end')) : 'desc',
|
||||
sortMetricNameList: rateMap[sorter?.field] || [],
|
||||
};
|
||||
|
||||
request(API.getMirrorMakerList(global?.clusterInfo?.id), { method: 'POST', data: params })
|
||||
// request(API.getConnectorsList(global?.clusterInfo?.id), { method: 'POST', data: params })
|
||||
.then((res: any) => {
|
||||
setPagination({
|
||||
current: res.pagination?.pageNo,
|
||||
pageSize: res.pagination?.pageSize,
|
||||
total: res.pagination?.total,
|
||||
});
|
||||
const newData =
|
||||
res?.bizData.map((item: any) => {
|
||||
return {
|
||||
...item,
|
||||
...item?.latestMetrics?.metrics,
|
||||
key: item.connectClusterName + item.connectorName,
|
||||
};
|
||||
}) || [];
|
||||
setData(newData);
|
||||
setLoading(false);
|
||||
})
|
||||
.catch((err) => {
|
||||
setLoading(false);
|
||||
});
|
||||
};
|
||||
|
||||
const onTableChange = (pagination: any, filters: any, sorter: any) => {
|
||||
setSortInfo(sorter);
|
||||
genData({ pageNo: pagination.current, pageSize: pagination.pageSize, filters, sorter });
|
||||
};
|
||||
|
||||
const menu = (
|
||||
<Menu className="">
|
||||
<Menu.Item>
|
||||
<span onClick={() => addConnectorJsonRef.current?.onOpen('create')}>JSON 新增MM2</span>
|
||||
</Menu.Item>
|
||||
</Menu>
|
||||
);
|
||||
|
||||
const getDetailInfo = (record: any) => {
|
||||
setDetailRecord(record);
|
||||
setDetailVisible(true);
|
||||
};
|
||||
|
||||
// 编辑
|
||||
const editConnector = (detail: OperateInfo['detail']) => {
|
||||
addConnectorRef.current?.onOpen('edit', addConnectorJsonRef.current, detail);
|
||||
};
|
||||
|
||||
// 重启、暂停/继续 操作
|
||||
const optionConnect = (record: any, action: string) => {
|
||||
setLoading(true);
|
||||
const params = {
|
||||
action,
|
||||
connectClusterId: record?.connectClusterId,
|
||||
connectorName: record?.connectorName,
|
||||
};
|
||||
|
||||
request(API.mirrorMakerOperates, { method: 'PUT', data: params })
|
||||
.then((res: any) => {
|
||||
if (res === null) {
|
||||
notification.success({
|
||||
message: `任务已${optionType[params.action]}`,
|
||||
description: `任务状态更新会有至多1min延迟`,
|
||||
});
|
||||
genData({ pageNo: pagination.current, pageSize: pagination.pageSize, sorter: sortInfo });
|
||||
setHealthType(!healthType);
|
||||
} else {
|
||||
notification.error({
|
||||
message: `${optionType[params.action]}任务失败`,
|
||||
});
|
||||
}
|
||||
})
|
||||
.catch((err) => {
|
||||
setLoading(false);
|
||||
});
|
||||
};
|
||||
|
||||
// 删除任务
|
||||
const deleteTesk = () => {
|
||||
genData({ pageNo: 1, pageSize: pagination.pageSize });
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
genData({
|
||||
pageNo: 1,
|
||||
pageSize: pagination.pageSize,
|
||||
sorter: sortInfo,
|
||||
});
|
||||
}, [searchKeywords]);
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className="breadcrumb" style={{ marginBottom: '10px' }}>
|
||||
<DBreadcrumb
|
||||
breadcrumbs={[
|
||||
{ label: '多集群管理', aHref: '/' },
|
||||
{ label: global?.clusterInfo?.name, aHref: `/cluster/${global?.clusterInfo?.id}` },
|
||||
{ label: 'Replication', aHref: `/cluster/${global?.clusterInfo?.id}/replication` },
|
||||
{ label: 'Mirror Maker', aHref: `` },
|
||||
]}
|
||||
/>
|
||||
</div>
|
||||
{/* <HasConnector>
|
||||
<>
|
||||
|
||||
</>
|
||||
</HasConnector> */}
|
||||
<div style={{ margin: '12px 0' }}>
|
||||
<MirrorMakerCard state={healthType} />
|
||||
</div>
|
||||
<div className="custom-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>
|
||||
</div>
|
||||
<div className={`${tableHeaderPrefix}-right`}>
|
||||
<SearchInput
|
||||
onSearch={setSearchKeywords}
|
||||
attrs={{
|
||||
placeholder: '请输入MM2 Name',
|
||||
style: { width: '248px', borderRiadus: '8px' },
|
||||
maxLength: 128,
|
||||
}}
|
||||
/>
|
||||
{global.hasPermission && global.hasPermission(ClustersPermissionMap.MM2_ADD) ? (
|
||||
<span className="add-connect">
|
||||
<Button
|
||||
className="add-connect-btn"
|
||||
icon={<IconFont type="icon-jiahao" />}
|
||||
type="primary"
|
||||
onClick={() => addConnectorRef.current?.onOpen('create', addConnectorJsonRef.current)}
|
||||
>
|
||||
新增MM2
|
||||
</Button>
|
||||
<Dropdown overlayClassName="add-connect-dropdown-menu" overlay={menu}>
|
||||
<Button className="add-connect-json" type="primary">
|
||||
<IconFont type="icon-guanwangxiala" />
|
||||
</Button>
|
||||
</Dropdown>
|
||||
</span>
|
||||
) : (
|
||||
<></>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
<ProTable
|
||||
key="mirror-maker-table"
|
||||
showQueryForm={false}
|
||||
tableProps={{
|
||||
showHeader: false,
|
||||
rowKey: 'key',
|
||||
loading: loading,
|
||||
columns: getMM2Columns({ getDetailInfo, deleteTesk, optionConnect, editConnector }),
|
||||
dataSource: data,
|
||||
paginationProps: { ...pagination },
|
||||
attrs: {
|
||||
onChange: onTableChange,
|
||||
scroll: { x: 'max-content', y: 'calc(100vh - 400px)' },
|
||||
bordered: false,
|
||||
},
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
<MM2Detail visible={detailVisible} setVisible={setDetailVisible} record={detailRecord} />
|
||||
<AddMM2
|
||||
ref={addConnectorRef}
|
||||
refresh={() => genData({ pageNo: pagination.current, pageSize: pagination.pageSize, sorter: sortInfo })}
|
||||
/>
|
||||
<AddConnectorUseJSON
|
||||
ref={addConnectorJsonRef}
|
||||
refresh={() => genData({ pageNo: pagination.current, pageSize: pagination.pageSize, sorter: sortInfo })}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default MirrorMaker2;
|
||||
@@ -29,6 +29,9 @@ import ConnectDashboard from './ConnectDashboard';
|
||||
import Connectors from './Connect';
|
||||
import Workers from './Connect/Workers';
|
||||
|
||||
import MirrorMaker2 from './MirrorMaker2';
|
||||
import MirrorMakerDashboard from './MirrorMakerDashBoard';
|
||||
|
||||
const pageRoutes = [
|
||||
{
|
||||
path: '/',
|
||||
@@ -152,6 +155,18 @@ const pageRoutes = [
|
||||
component: Workers,
|
||||
noSider: false,
|
||||
},
|
||||
{
|
||||
path: 'replication',
|
||||
exact: true,
|
||||
component: MirrorMakerDashboard,
|
||||
noSider: false,
|
||||
},
|
||||
{
|
||||
path: 'replication/mirror-maker',
|
||||
exact: true,
|
||||
component: MirrorMaker2,
|
||||
noSider: false,
|
||||
},
|
||||
{
|
||||
path: 'security/acls',
|
||||
exact: true,
|
||||
|
||||
Reference in New Issue
Block a user