mirror of
https://github.com/didi/KnowStreaming.git
synced 2025-12-24 20:22:12 +08:00
feat: 新增Mirror Maker 2.0(MM2)
This commit is contained in:
@@ -18,6 +18,7 @@ export enum MetricType {
|
|||||||
Connect = 120,
|
Connect = 120,
|
||||||
Connectors = 121,
|
Connectors = 121,
|
||||||
Controls = 901,
|
Controls = 901,
|
||||||
|
MM2 = 122,
|
||||||
}
|
}
|
||||||
|
|
||||||
const api = {
|
const api = {
|
||||||
@@ -233,9 +234,9 @@ const api = {
|
|||||||
getConnectors: (clusterPhyId: string) => getApi(`/clusters/${clusterPhyId}/connectors-basic`),
|
getConnectors: (clusterPhyId: string) => getApi(`/clusters/${clusterPhyId}/connectors-basic`),
|
||||||
getConnectorMetrics: (clusterPhyId: string) => getApi(`/clusters/${clusterPhyId}/connectors-metrics`),
|
getConnectorMetrics: (clusterPhyId: string) => getApi(`/clusters/${clusterPhyId}/connectors-metrics`),
|
||||||
getConnectorPlugins: (connectClusterId: number) => getApi(`/kafka-connect/clusters/${connectClusterId}/connector-plugins`),
|
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`),
|
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`),
|
getApi(`/kafka-connect/clusters/${connectClusterId}/connectors/${connectorName}/config`),
|
||||||
isConnectorExist: (connectClusterId: number, connectorName: string) =>
|
isConnectorExist: (connectClusterId: number, connectorName: string) =>
|
||||||
getApi(`/kafka-connect/clusters/${connectClusterId}/connectors/${connectorName}/basic-combine-exist`),
|
getApi(`/kafka-connect/clusters/${connectClusterId}/connectors/${connectorName}/basic-combine-exist`),
|
||||||
@@ -251,6 +252,39 @@ const api = {
|
|||||||
|
|
||||||
getConnectClusterBasicExit: (clusterPhyId: string, clusterPhyName: string) =>
|
getConnectClusterBasicExit: (clusterPhyId: string, clusterPhyName: string) =>
|
||||||
getApi(`/kafka-clusters/${clusterPhyId}/connect-clusters/${clusterPhyName}/basic-combine-exist`),
|
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;
|
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[];
|
cardColumns?: any[];
|
||||||
healthData?: healthDataProps;
|
healthData?: healthDataProps;
|
||||||
showCardBg?: boolean;
|
showCardBg?: boolean;
|
||||||
scene: 'topics' | 'brokers' | 'topic' | 'broker' | 'group' | 'zookeeper' | 'connect' | 'connector';
|
scene: 'topics' | 'brokers' | 'topic' | 'broker' | 'group' | 'zookeeper' | 'connect' | 'connector' | 'mm2';
|
||||||
record?: any;
|
record?: any;
|
||||||
loading?: boolean;
|
loading?: boolean;
|
||||||
needProgress?: boolean;
|
needProgress?: boolean;
|
||||||
@@ -67,6 +67,11 @@ const sceneCodeMap = {
|
|||||||
fieldName: 'connectorName',
|
fieldName: 'connectorName',
|
||||||
alias: 'Connector',
|
alias: 'Connector',
|
||||||
},
|
},
|
||||||
|
mm2: {
|
||||||
|
code: 7,
|
||||||
|
fieldName: 'connectorName',
|
||||||
|
alias: 'MM2',
|
||||||
|
},
|
||||||
};
|
};
|
||||||
const CardColumnsItem: any = (cardItem: any) => {
|
const CardColumnsItem: any = (cardItem: any) => {
|
||||||
const { cardColumnsItemData, showCardBg } = cardItem;
|
const { cardColumnsItemData, showCardBg } = cardItem;
|
||||||
@@ -108,7 +113,7 @@ const CardBar = (props: CardBarProps) => {
|
|||||||
const sceneObj = sceneCodeMap[scene];
|
const sceneObj = sceneCodeMap[scene];
|
||||||
const path = record
|
const path = record
|
||||||
? api.getResourceHealthDetail(
|
? api.getResourceHealthDetail(
|
||||||
scene === 'connector' ? Number(record?.connectClusterId) : Number(routeParams.clusterId),
|
scene === 'connector' || scene === 'mm2' ? Number(record?.connectClusterId) : Number(routeParams.clusterId),
|
||||||
sceneObj.code,
|
sceneObj.code,
|
||||||
record[sceneObj.fieldName]
|
record[sceneObj.fieldName]
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -89,7 +89,15 @@ export const MetricSelect = forwardRef((metricSelect: MetricSelectProps, ref) =>
|
|||||||
|
|
||||||
const columns = [
|
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',
|
dataIndex: 'category',
|
||||||
key: 'category',
|
key: 'category',
|
||||||
},
|
},
|
||||||
@@ -112,7 +120,7 @@ export const MetricSelect = forwardRef((metricSelect: MetricSelectProps, ref) =>
|
|||||||
desc,
|
desc,
|
||||||
unit: metricDefine?.unit,
|
unit: metricDefine?.unit,
|
||||||
};
|
};
|
||||||
if (metricDefine.category) {
|
if (metricDefine?.category) {
|
||||||
if (!categoryData[metricDefine.category]) {
|
if (!categoryData[metricDefine.category]) {
|
||||||
categoryData[metricDefine.category] = [returnData];
|
categoryData[metricDefine.category] = [returnData];
|
||||||
} else {
|
} else {
|
||||||
@@ -129,11 +137,11 @@ export const MetricSelect = forwardRef((metricSelect: MetricSelectProps, ref) =>
|
|||||||
};
|
};
|
||||||
|
|
||||||
const formateSelectedKeys = () => {
|
const formateSelectedKeys = () => {
|
||||||
const newKeys = metricSelect.selectedRows;
|
const newKeys = metricSelect?.selectedRows;
|
||||||
const result: SelectedMetrics = {};
|
const result: SelectedMetrics = {};
|
||||||
const selectedCategories: string[] = [];
|
const selectedCategories: string[] = [];
|
||||||
|
|
||||||
newKeys.forEach((name: string) => {
|
newKeys?.forEach((name: string) => {
|
||||||
const metricDefine = global.getMetricDefine(metricSelect?.metricType, name);
|
const metricDefine = global.getMetricDefine(metricSelect?.metricType, name);
|
||||||
if (metricDefine) {
|
if (metricDefine) {
|
||||||
if (!result[metricDefine.category]) {
|
if (!result[metricDefine.category]) {
|
||||||
|
|||||||
@@ -167,6 +167,9 @@ const ChartDetail = (props: ChartDetailProps) => {
|
|||||||
const getMetricChartData = ([startTime, endTime]: readonly [number, number]) => {
|
const getMetricChartData = ([startTime, endTime]: readonly [number, number]) => {
|
||||||
const getQueryUrl = () => {
|
const getQueryUrl = () => {
|
||||||
switch (metricType) {
|
switch (metricType) {
|
||||||
|
case MetricType.MM2: {
|
||||||
|
return api.getMirrorMakerMetrics(clusterId);
|
||||||
|
}
|
||||||
case MetricType.Connect: {
|
case MetricType.Connect: {
|
||||||
return api.getConnectClusterMetrics(clusterId);
|
return api.getConnectClusterMetrics(clusterId);
|
||||||
}
|
}
|
||||||
@@ -180,13 +183,16 @@ const ChartDetail = (props: ChartDetailProps) => {
|
|||||||
[MetricType.Topic]: 'topics',
|
[MetricType.Topic]: 'topics',
|
||||||
[MetricType.Connect]: 'connectClusterIdList',
|
[MetricType.Connect]: 'connectClusterIdList',
|
||||||
[MetricType.Connectors]: 'connectorNameList',
|
[MetricType.Connectors]: 'connectorNameList',
|
||||||
|
[MetricType.MM2]: 'connectorNameList',
|
||||||
};
|
};
|
||||||
|
|
||||||
return Utils.post(getQueryUrl(), {
|
return Utils.post(getQueryUrl(), {
|
||||||
startTime,
|
startTime,
|
||||||
endTime,
|
endTime,
|
||||||
metricsNames: [metricName],
|
metricsNames: [metricName],
|
||||||
topNu: null,
|
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.Broker]: (clusterId: string) => api.getDashboardMetricChartData(clusterId, MetricType.Broker),
|
||||||
[MetricType.Topic]: (clusterId: string) => api.getDashboardMetricChartData(clusterId, MetricType.Topic),
|
[MetricType.Topic]: (clusterId: string) => api.getDashboardMetricChartData(clusterId, MetricType.Topic),
|
||||||
[MetricType.Zookeeper]: (clusterId: string) => api.getZookeeperMetrics(clusterId),
|
[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) =>
|
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 getScopeList = async () => {
|
||||||
const res: any = await Utils.request(api.getDashboardMetadata(clusterId, dashboardType));
|
const res: any = await Utils.request(
|
||||||
const list = res.map((item: any) => {
|
dashboardType !== MetricType.MM2 ? api.getDashboardMetadata(clusterId, dashboardType) : api.getMirrorMakerMetadata(clusterId)
|
||||||
return dashboardType === MetricType.Broker
|
);
|
||||||
? {
|
const mockRes = [{ connectClusterId: 1, connectClusterName: 'connectClusterName', connectorName: 'connectorName' }];
|
||||||
label: item.host,
|
const list =
|
||||||
value: item.brokerId,
|
res.length > 0
|
||||||
}
|
? res.map((item: any) => {
|
||||||
: {
|
return dashboardType === MetricType.Broker
|
||||||
label: item.topicName,
|
? {
|
||||||
value: item.topicName,
|
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);
|
setScopeList(list);
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -172,19 +189,23 @@ const DraggableCharts = (props: PropsType): JSX.Element => {
|
|||||||
const [startTime, endTime] = curHeaderOptions.rangeTime;
|
const [startTime, endTime] = curHeaderOptions.rangeTime;
|
||||||
const curTimestamp = Date.now();
|
const curTimestamp = Date.now();
|
||||||
curFetchingTimestamp.current = curTimestamp;
|
curFetchingTimestamp.current = curTimestamp;
|
||||||
|
|
||||||
const reqBody = Object.assign(
|
const reqBody = Object.assign(
|
||||||
{
|
{
|
||||||
startTime,
|
startTime,
|
||||||
endTime,
|
endTime,
|
||||||
metricsNames: metricList || [],
|
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,
|
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']:
|
||||||
? null
|
curHeaderOptions?.scopeData?.isTop
|
||||||
: curHeaderOptions.scopeData.data,
|
? 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,
|
dashboardType,
|
||||||
curHeaderOptions.rangeTime
|
curHeaderOptions.rangeTime
|
||||||
) as FormattedMetricData[];
|
) 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));
|
formattedMetricData.sort((a, b) => metricRankList.current.indexOf(a.metricName) - metricRankList.current.indexOf(b.metricName));
|
||||||
|
const filterNullformattedMetricData = nullformattedMetricData.filter((item: any) => item !== null);
|
||||||
setMetricChartData(formattedMetricData);
|
filterNullformattedMetricData.sort(
|
||||||
|
(a: any, b: any) => metricRankList.current.indexOf(a?.metricName) - metricRankList.current.indexOf(b?.metricName)
|
||||||
|
);
|
||||||
|
setMetricChartData([...formattedMetricData, ...filterNullformattedMetricData]);
|
||||||
}
|
}
|
||||||
setLoading(false);
|
setLoading(false);
|
||||||
},
|
},
|
||||||
@@ -255,12 +297,15 @@ const DraggableCharts = (props: PropsType): JSX.Element => {
|
|||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (metricList?.length && curHeaderOptions) {
|
if (metricList?.length && curHeaderOptions) {
|
||||||
getMetricChartData();
|
getMetricChartData();
|
||||||
|
} else {
|
||||||
|
setMetricChartData([]);
|
||||||
|
setLoading(false);
|
||||||
}
|
}
|
||||||
}, [curHeaderOptions, metricList]);
|
}, [curHeaderOptions, metricList]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
// 初始化页面,获取 scope 和 metric 信息
|
// 初始化页面,获取 scope 和 metric 信息
|
||||||
(dashboardType === MetricType.Broker || dashboardType === MetricType.Topic) && getScopeList();
|
(dashboardType === MetricType.Broker || dashboardType === MetricType.Topic || dashboardType === MetricType.MM2) && getScopeList();
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@@ -270,11 +315,24 @@ const DraggableCharts = (props: PropsType): JSX.Element => {
|
|||||||
hideNodeScope={dashboardType === MetricType.Zookeeper}
|
hideNodeScope={dashboardType === MetricType.Zookeeper}
|
||||||
openMetricFilter={() => metricFilterRef.current?.open()}
|
openMetricFilter={() => metricFilterRef.current?.open()}
|
||||||
nodeSelect={{
|
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: (
|
customContent: (
|
||||||
<SelectContent
|
<SelectContent
|
||||||
title={`自定义 ${
|
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}
|
list={scopeList}
|
||||||
/>
|
/>
|
||||||
|
|||||||
@@ -79,7 +79,6 @@ export const leftMenus = (clusterId?: string, clusterRunState?: number) => ({
|
|||||||
return (
|
return (
|
||||||
<div className="menu-item-with-beta-tag">
|
<div className="menu-item-with-beta-tag">
|
||||||
<span>{intl.formatMessage({ id: 'menu.cluster.connect' })}</span>
|
<span>{intl.formatMessage({ id: 'menu.cluster.connect' })}</span>
|
||||||
<div className="beta-tag"></div>
|
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
@@ -103,6 +102,30 @@ export const leftMenus = (clusterId?: string, clusterRunState?: number) => ({
|
|||||||
},
|
},
|
||||||
].filter((m) => m),
|
].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',
|
name: 'consumer-group',
|
||||||
path: 'consumers',
|
path: 'consumers',
|
||||||
|
|||||||
@@ -52,6 +52,10 @@ export default {
|
|||||||
[`menu.${systemKey}.connect.connectors`]: 'Connectors',
|
[`menu.${systemKey}.connect.connectors`]: 'Connectors',
|
||||||
[`menu.${systemKey}.connect.workers`]: 'Workers',
|
[`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}.acls`]: 'ACLs',
|
||||||
|
|
||||||
[`menu.${systemKey}.jobs`]: 'Job',
|
[`menu.${systemKey}.jobs`]: 'Job',
|
||||||
|
|||||||
@@ -26,11 +26,19 @@ export enum ClustersPermissionMap {
|
|||||||
TOPIC_ADD = 'Topic-新增Topic',
|
TOPIC_ADD = 'Topic-新增Topic',
|
||||||
TOPIC_MOVE_REPLICA = 'Topic-迁移副本',
|
TOPIC_MOVE_REPLICA = 'Topic-迁移副本',
|
||||||
TOPIC_CHANGE_REPLICA = 'Topic-扩缩副本',
|
TOPIC_CHANGE_REPLICA = 'Topic-扩缩副本',
|
||||||
|
TOPIC_REPLICATOR = 'Topic-新增Topic复制',
|
||||||
|
TOPIC_CANCEL_REPLICATOR = 'Topic-详情-取消Topic复制',
|
||||||
// Consumers
|
// Consumers
|
||||||
CONSUMERS_RESET_OFFSET = 'Consumers-重置Offset',
|
CONSUMERS_RESET_OFFSET = 'Consumers-重置Offset',
|
||||||
// Test
|
// Test
|
||||||
TEST_CONSUMER = 'Test-Consumer',
|
TEST_CONSUMER = 'Test-Consumer',
|
||||||
TEST_PRODUCER = 'Test-Producer',
|
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 {
|
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 Connectors from './Connect';
|
||||||
import Workers from './Connect/Workers';
|
import Workers from './Connect/Workers';
|
||||||
|
|
||||||
|
import MirrorMaker2 from './MirrorMaker2';
|
||||||
|
import MirrorMakerDashboard from './MirrorMakerDashBoard';
|
||||||
|
|
||||||
const pageRoutes = [
|
const pageRoutes = [
|
||||||
{
|
{
|
||||||
path: '/',
|
path: '/',
|
||||||
@@ -152,6 +155,18 @@ const pageRoutes = [
|
|||||||
component: Workers,
|
component: Workers,
|
||||||
noSider: false,
|
noSider: false,
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
path: 'replication',
|
||||||
|
exact: true,
|
||||||
|
component: MirrorMakerDashboard,
|
||||||
|
noSider: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: 'replication/mirror-maker',
|
||||||
|
exact: true,
|
||||||
|
component: MirrorMaker2,
|
||||||
|
noSider: false,
|
||||||
|
},
|
||||||
{
|
{
|
||||||
path: 'security/acls',
|
path: 'security/acls',
|
||||||
exact: true,
|
exact: true,
|
||||||
|
|||||||
Reference in New Issue
Block a user