feat: 新增Mirror Maker 2.0(MM2)

This commit is contained in:
wyb
2023-02-10 16:39:47 +08:00
committed by lucasun
parent f03460f3cd
commit fa2abadc25
19 changed files with 3096 additions and 32 deletions

View File

@@ -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;

View File

@@ -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;

View File

@@ -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;

View File

@@ -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]
)

View File

@@ -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]) {

View File

@@ -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,
});
};

View File

@@ -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) =>

View File

@@ -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}
/>

View File

@@ -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',

View File

@@ -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',

View File

@@ -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

View File

@@ -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>
);
});

View File

@@ -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;

View File

@@ -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;

View File

@@ -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;
};

View File

@@ -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;
}
}
}

View File

@@ -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;

View File

@@ -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,