diff --git a/km-console/packages/layout-clusters-fe/src/api/index.ts b/km-console/packages/layout-clusters-fe/src/api/index.ts index 4d185ea9..1c6bc77e 100755 --- a/km-console/packages/layout-clusters-fe/src/api/index.ts +++ b/km-console/packages/layout-clusters-fe/src/api/index.ts @@ -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; diff --git a/km-console/packages/layout-clusters-fe/src/components/CardBar/MirrorMakerCard.tsx b/km-console/packages/layout-clusters-fe/src/components/CardBar/MirrorMakerCard.tsx new file mode 100644 index 00000000..f7489e75 --- /dev/null +++ b/km-console/packages/layout-clusters-fe/src/components/CardBar/MirrorMakerCard.tsx @@ -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({ + 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 ( +
+ Connectors + + + +
+ ); + }, + value() { + return ( + + {getVal(aliveConnectorCount)}/{getVal(totalConnectorCount)} + + ); + }, + }, + { + title() { + return ( +
+ Tasks + + + +
+ ); + }, + value() { + return ( + + {getVal(aliveTaskCount)}/{getVal(totalTaskCount)} + + ); + }, + }, + ]; + setCardData(cardMap); + }); + }; + useEffect(() => { + setLoading(true); + Promise.all([getHealthData(), getCardInfo()]).finally(() => { + setLoading(false); + }); + }, [clusterId, state]); + return ; +}; + +export default ConnectCard; diff --git a/km-console/packages/layout-clusters-fe/src/components/CardBar/MirrorMakerDetailCard.tsx b/km-console/packages/layout-clusters-fe/src/components/CardBar/MirrorMakerDetailCard.tsx new file mode 100644 index 00000000..743e2e12 --- /dev/null +++ b/km-console/packages/layout-clusters-fe/src/components/CardBar/MirrorMakerDetailCard.tsx @@ -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(); + const [loading, setLoading] = useState(false); + const [cardData, setCardData] = useState([]); + const [healthData, setHealthData] = useState({ + 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 ( + <> + { + + {Utils.firstCharUppercase(state) || '-'} + + } + + ); + }, + }, + + { + title() { + return ( +
+ Tasks + + + +
+ ); + }, + value() { + return ( + + {getVal(aliveTaskCount)}/{getVal(totalTaskCount)} + + ); + }, + }, + { + title: 'Workers', + value: getVal(totalWorkerCount), + }, + ]; + setCardData(cordRightMap); + }); + }; + + const noDataCardInfo = () => { + const cordRightMap = [ + { + title: 'Status', + // value: Utils.firstCharUppercase(state) || '-', + value() { + return -; + }, + }, + + { + title() { + return ( +
+ Tasks + + + +
+ ); + }, + value() { + return -/-; + }, + }, + { + title: 'Workers', + value() { + return -; + }, + }, + ]; + 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 ( + + ); +}; + +export default ConnectDetailCard; diff --git a/km-console/packages/layout-clusters-fe/src/components/CardBar/index.tsx b/km-console/packages/layout-clusters-fe/src/components/CardBar/index.tsx index f35de692..ea683e07 100644 --- a/km-console/packages/layout-clusters-fe/src/components/CardBar/index.tsx +++ b/km-console/packages/layout-clusters-fe/src/components/CardBar/index.tsx @@ -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] ) diff --git a/km-console/packages/layout-clusters-fe/src/components/ChartOperateBar/MetricSelect.tsx b/km-console/packages/layout-clusters-fe/src/components/ChartOperateBar/MetricSelect.tsx index 628275cd..3ce20f3c 100644 --- a/km-console/packages/layout-clusters-fe/src/components/ChartOperateBar/MetricSelect.tsx +++ b/km-console/packages/layout-clusters-fe/src/components/ChartOperateBar/MetricSelect.tsx @@ -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]) { diff --git a/km-console/packages/layout-clusters-fe/src/components/DraggableCharts/Detail.tsx b/km-console/packages/layout-clusters-fe/src/components/DraggableCharts/Detail.tsx index 8b04205a..c635d0e0 100644 --- a/km-console/packages/layout-clusters-fe/src/components/DraggableCharts/Detail.tsx +++ b/km-console/packages/layout-clusters-fe/src/components/DraggableCharts/Detail.tsx @@ -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, }); }; diff --git a/km-console/packages/layout-clusters-fe/src/components/DraggableCharts/config.tsx b/km-console/packages/layout-clusters-fe/src/components/DraggableCharts/config.tsx index 3a60eca4..87da8dab 100644 --- a/km-console/packages/layout-clusters-fe/src/components/DraggableCharts/config.tsx +++ b/km-console/packages/layout-clusters-fe/src/components/DraggableCharts/config.tsx @@ -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) => diff --git a/km-console/packages/layout-clusters-fe/src/components/DraggableCharts/index.tsx b/km-console/packages/layout-clusters-fe/src/components/DraggableCharts/index.tsx index 83422932..52e82935 100644 --- a/km-console/packages/layout-clusters-fe/src/components/DraggableCharts/index.tsx +++ b/km-console/packages/layout-clusters-fe/src/components/DraggableCharts/index.tsx @@ -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) => { - return dashboardType === MetricType.Broker - ? { - label: item.host, - value: item.brokerId, - } - : { - label: item.topicName, - value: item.topicName, - }; - }); + 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,19 +189,23 @@ 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 - ? null - : curHeaderOptions.scopeData.data, + [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: ( diff --git a/km-console/packages/layout-clusters-fe/src/constants/menu.tsx b/km-console/packages/layout-clusters-fe/src/constants/menu.tsx index d0c0b592..47d84df4 100755 --- a/km-console/packages/layout-clusters-fe/src/constants/menu.tsx +++ b/km-console/packages/layout-clusters-fe/src/constants/menu.tsx @@ -79,7 +79,6 @@ export const leftMenus = (clusterId?: string, clusterRunState?: number) => ({ return (
{intl.formatMessage({ id: 'menu.cluster.connect' })} -
); }, @@ -103,6 +102,30 @@ export const leftMenus = (clusterId?: string, clusterRunState?: number) => ({ }, ].filter((m) => m), }, + { + name: (intl: any) => { + return ( +
+ {intl.formatMessage({ id: 'menu.cluster.replication' })} +
+
+ ); + }, + path: 'replication', + icon: 'icon-Operation', + children: [ + { + name: (intl: any) => {intl.formatMessage({ id: 'menu.cluster.replication.dashboard' })}, + path: '', + icon: 'icon-luoji', + }, + { + name: (intl: any) => {intl.formatMessage({ id: 'menu.cluster.replication.mirror-maker' })}, + path: 'mirror-maker', + icon: '#icon-luoji', + }, + ].filter((m) => m), + }, { name: 'consumer-group', path: 'consumers', diff --git a/km-console/packages/layout-clusters-fe/src/locales/zh.tsx b/km-console/packages/layout-clusters-fe/src/locales/zh.tsx index 5537c9d0..493a4bd9 100755 --- a/km-console/packages/layout-clusters-fe/src/locales/zh.tsx +++ b/km-console/packages/layout-clusters-fe/src/locales/zh.tsx @@ -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', diff --git a/km-console/packages/layout-clusters-fe/src/pages/CommonConfig.tsx b/km-console/packages/layout-clusters-fe/src/pages/CommonConfig.tsx index 8e37edf3..4d01da97 100644 --- a/km-console/packages/layout-clusters-fe/src/pages/CommonConfig.tsx +++ b/km-console/packages/layout-clusters-fe/src/pages/CommonConfig.tsx @@ -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 { diff --git a/km-console/packages/layout-clusters-fe/src/pages/MirrorMaker2/AddMM2.tsx b/km-console/packages/layout-clusters-fe/src/pages/MirrorMaker2/AddMM2.tsx new file mode 100644 index 00000000..df7d13e2 --- /dev/null +++ b/km-console/packages/layout-clusters-fe/src/pages/MirrorMaker2/AddMM2.tsx @@ -0,0 +1,1239 @@ +import React, { createContext, createElement, forwardRef, useContext, useEffect, useImperativeHandle, useRef, useState } from 'react'; +import { Alert, Button, Drawer, Form, Input, InputNumber, Radio, Select, Steps, Switch, Table, Transfer, Utils } from 'knowdesign'; +import { FormInstance } from 'knowdesign/es/basic/form/Form'; +import message from '@src/components/Message'; +import api from '@src/api'; +import { useParams } from 'react-router-dom'; +import { IconFont } from '@knowdesign/icons'; +import { regClusterName } from '@src/constants/reg'; + +const { Step } = Steps; + +export interface ConnectCluster { + id: number; + name: string; + groupName: string; + state: number; + version: string; + jmxProperties: string; + clusterUrl: string; + memberLeaderUrl: string; +} + +export interface ConnectorPlugin { + type: 'source' | 'sink'; + version: string; + className: string; + helpDocLink: string; +} + +interface ConnectorPluginConfigDefinition { + name: string; + type: string; + required: boolean; + defaultValue: string | null; + importance: string; + documentation: string; + group: string; + orderInGroup: number; + width: string; + displayName: string; + dependents: string[]; +} + +interface ConnectorPluginConfigValue { + errors: string[]; + name: string; + recommendedValues: any[]; + value: any; + visible: boolean; +} + +export interface ConnectorPluginConfig { + name: string; + errorCount: number; + groups: string[]; + configs: { + definition: ConnectorPluginConfigDefinition; + value: ConnectorPluginConfigValue; + }[]; +} + +interface FormConnectorConfigs { + pluginConfig: { [key: string]: ConnectorPluginConfigDefinition[] }; + connectorConfig?: { [key: string]: any }; +} + +interface SubFormProps { + visible: boolean; + setSubmitLoading: (loading: boolean) => void; +} + +export interface OperateInfo { + type: 'create' | 'edit'; + errors: { + [key: string]: string[]; + }; + detail?: { + connectClusterId: number; + connectorName: string; + connectorClassName: string; + connectorType: 'source' | 'sink'; + }; + setSourceKafkaClusterId?: any; + setBootstrapServers?: any; + setSourceDetailConfigs?: any; + setCheckoutPointDetailConfigs?: any; + setHeartbeatDetailConfigs?: any; +} + +const existConfigItems = { + sourceConfigs: [ + 'sync.topic.configs.enabled', + 'sync.topic.configs.interval.seconds', + 'sync.topic.acls.enabled', + 'sync.topic.acls.interval.seconds', + 'refresh.topics.enabled', + 'refresh.topics.interval.seconds', + 'refresh.groups.enabled', + 'refresh.groups.interval.seconds', + 'replication.policy.separator', + 'replication.policy.class', + 'topics', + ], + checkpointConfig: ['emit.checkpoints.enabled', 'emit.checkpoints.interval.seconds', 'checkpoints.topic.replication.factor'], + heartbeatConfig: ['heartbeats.topic.replication.factor', 'emit.heartbeats.interval.seconds'], +}; + +const StepsFormContent = createContext< + OperateInfo & { + forms: { current: { [key: string]: FormInstance } }; + } +>({ + type: 'create', + errors: {}, + forms: { current: {} }, +}); + +function useStepForm(key: string | number) { + const { forms } = useContext(StepsFormContent); + const [form] = Form.useForm(); + let formInstace = form; + + if (forms.current[key]) { + formInstace = forms.current[key] as FormInstance; + } else { + forms.current[key] = formInstace; + } + + return [formInstace]; +} + +// 步骤一 +const StepFormFirst = (props: SubFormProps) => { + const { clusterId } = useParams<{ + clusterId: string; + }>(); + const [form] = useStepForm(0); + const [topicData, setTopicData] = useState([]); + const { type, detail, setSourceKafkaClusterId, setBootstrapServers, setSourceDetailConfigs, setCheckoutPointDetailConfigs } = + useContext(StepsFormContent); + const isEdit = type === 'edit'; + const [seniorConfig, setSeniorConfig] = useState(false); + const [topicTargetKeys, setTopicTargetKeys] = useState([]); + const [connectClusters, setConnectClusters] = useState<{ label: string; value: number }[]>([]); + const [sourcekafkaClusters, setSourcekafkaClusters] = useState<{ label: string; value: number }[]>([]); + const [headSourcekafkaClusters, setHeadSourcekafkaClusters] = useState([]); + const [formItemValues, setFormItemValues] = useState([]); + const [formItemValue, setFormItemValue] = useState({}); + const [sourcekafkaClustersId, setSourcekafkaClustersId] = useState(null); + + const topicChange = (val: any) => { + setTopicTargetKeys(val); + }; + + // 获取Connect基本信息列表 + const getConnectClustersList = () => { + Utils.request(api.getConnectClusters(clusterId)).then((res: any) => { + const arr = res.map(({ name, id }: any) => ({ + label: name || '-', + value: id, + })); + setConnectClusters(arr); + }); + }; + + // 获取 Source Kafka 集群列表 + const getSourceKafkaClustersList = () => { + Utils.request(api.getSourceKafkaClusterBasic).then((res: any) => { + setHeadSourcekafkaClusters(res || []); + const arr = res + .filter((o: any) => o.id !== +clusterId) + .map(({ name, id }: any) => ({ + label: name || '-', + value: id, + })); + setSourcekafkaClusters(arr); + }); + }; + + // 获取Topic列表 + const getTopicList = () => { + // ! 需整理 + Utils.request(api.getTopicMetaList(Number(clusterId))).then((res: any) => { + const dataDe = res || []; + const dataHandle = dataDe.map((item: any) => { + return { + ...item, + key: item.topicName, + label: item.topicName, + value: item.topicName, + title: item.topicName, + }; + }); + setTopicData(dataHandle); + }); + }; + + const getMM2Config = (connectClusterId: string | number) => { + Promise.all( + [ + Utils.request(api.getConnectorPluginConfig(connectClusterId, 'org.apache.kafka.connect.mirror.MirrorSourceConnector')), + Utils.request(api.getConnectorPluginConfig(connectClusterId, 'org.apache.kafka.connect.mirror.MirrorCheckpointConnector')), + isEdit ? Utils.request(api.getMirrorMakerConfig(connectClusterId, detail.connectorName)) : undefined, + ].filter((r) => r) + ) + .then((res: any) => { + const detailConfigs: any[] = isEdit && res.length > 1 ? res?.[2] : []; + let sourceConfigs: any; + let checkpointConfigs: any; + + detailConfigs?.forEach((config) => { + if (config['connector.class'] === 'org.apache.kafka.connect.mirror.MirrorCheckpointConnector') { + checkpointConfigs = config; + setCheckoutPointDetailConfigs(config); + } else if (config['connector.class'] === 'org.apache.kafka.connect.mirror.MirrorSourceConnector') { + sourceConfigs = config; + setSourceDetailConfigs(config); + } + }); + const formItemValue: any = {}; + const formItemValues: any = []; + res?.[0].configs.forEach(({ definition }: any) => { + if (existConfigItems.sourceConfigs.includes(definition.name)) { + if (isEdit && sourceConfigs[definition.name]) { + formItemValue[definition.name] = sourceConfigs[definition.name]; + if (definition.name === 'topics') { + sourceConfigs[definition.name] !== '.*' + ? formItemValues.push({ + name: 'topics', + value: sourceConfigs[definition.name].split(',') || null, + }) + : formItemValues.push({ + name: 'priority', + value: 'allTopic', + }); + } else { + formItemValues.push({ + name: definition.name, + value: sourceConfigs[definition.name] || null, + }); + } + } else { + formItemValue[definition.name] = definition.defaultValue; + if (definition.name === 'topics') { + definition.defaultValue !== '.*' + ? formItemValues.push({ + name: 'topics', + value: definition.defaultValue.split(',') || null, + }) + : formItemValues.push({ + name: 'priority', + value: 'allTopic', + }); + } else { + formItemValues.push({ + name: definition.name, + value: definition.defaultValue || null, + }); + } + } + } + }); + res?.[1].configs.forEach(({ definition }: any) => { + if (existConfigItems.checkpointConfig.includes(definition.name)) { + if (isEdit && checkpointConfigs[definition.name]) { + formItemValue[definition.name] = checkpointConfigs[definition.name]; + formItemValues.push({ + name: definition.name, + value: checkpointConfigs[definition.name] || null, + }); + } else { + formItemValue[definition.name] = definition.defaultValue; + formItemValues.push({ + name: definition.name, + value: definition.defaultValue || null, + }); + } + // formItemValue[definition.name] = + // definition.type === 'BOOLEAN' ? Boolean(definition.defaultValue) : definition.defaultValue || null; + // formItemValues.push({ + // name: definition.name, + // value: definition.type === 'BOOLEAN' ? Boolean(definition.defaultValue) : definition.defaultValue || null, + // }); + } + }); + setFormItemValue(formItemValue); + setFormItemValues(formItemValues); + }) + .finally(() => props.setSubmitLoading(false)); + }; + + useEffect(() => { + getTopicList(); + getConnectClustersList(); + getSourceKafkaClustersList(); + }, []); + + useEffect(() => { + form.resetFields(existConfigItems.sourceConfigs); + form.setFields(formItemValues); + }, [formItemValues]); + + useEffect(() => { + const bootstrapServers = + headSourcekafkaClusters.length && headSourcekafkaClusters.find((item) => item.id === sourcekafkaClustersId)?.bootstrapServers; + setBootstrapServers(bootstrapServers); + }, [sourcekafkaClustersId]); + + // useEffect(() => { + // connectorConfig && + // form.setFieldsValue({ + // topics: + // typeof connectorConfig['topics'] === 'string' ? connectorConfig['topics'].split(',').map((i: string) => i.trim()) : undefined, + // }); + // }, [topicData, connectorConfig]); + + useEffect(() => { + // 需要处理Topic和config配置,还需要考虑编辑时的回显问题 + isEdit && getMM2Config(detail.connectClusterId); + const config = isEdit + ? detail + : { + 'sync.topic.configs.enabled': false, + 'sync.topic.configs.interval.seconds': 0, + 'sync.topic.acls.enabled': false, + 'sync.topic.acls.interval.seconds': 600, + 'refresh.topics.enabled': false, + 'refresh.topics.interval.seconds': 600, + 'refresh.groups.enabled': false, + 'refresh.groups.interval.seconds': 600, + 'emit.checkpoints.enabled': false, + 'emit.checkpoints.interval.seconds': 60, + 'checkpoints.topic.replication.factor': false, + 'replication.policy.separator': '.', + priority: 'allTopic', + }; + form.setFieldsValue(config); + setFormItemValue((state: any) => { + return { ...state, ...config }; + }); + }, [isEdit, detail]); + + const onConnectClusterChange = (value: string) => { + value && getMM2Config(value); + }; + + return ( +
+
+ 64) { + return Promise.reject('MM2任务名称长度限制在1~64字符'); + } + if (!new RegExp(regClusterName).test(value)) { + return Promise.reject( + 'MM2 名称支持中英文、数字、特殊字符 ! " # $ % & \' ( ) * + , - . / : ; < = > ? @ [ ] ^ _ ` { | } ~' + ); + } + return Promise.resolve(); + // return Utils.request(api.isConnectorExist(prevForm.getFieldValue('connectClusterId'), value)).then( + // (res: any) => { + // const data = res || {}; + // return data?.exist ? Promise.reject('MM2 名称重复') : Promise.resolve(); + // }, + // () => Promise.reject('连接超时! 请重试或检查服务') + // ); + }, + }, + ]} + > + + + + + + {!seniorConfig && ( +
setSeniorConfig(true)}> + 高级配置 + +
+ )} +
+
+
+ + + setFormItemValue((state: any) => { + return { ...state, 'sync.topic.configs.enabled': e }; + }) + } + /> + + {formItemValue['sync.topic.configs.enabled'] ? ( + + + + ) : null} +
+
+ + + setFormItemValue((state: any) => { + return { ...state, 'sync.topic.acls.enabled': e }; + }) + } + /> + + {formItemValue['sync.topic.acls.enabled'] ? ( + + + + ) : null} +
+
+ + + setFormItemValue((state: any) => { + return { ...state, 'refresh.topics.enabled': e }; + }) + } + /> + + {formItemValue['refresh.topics.enabled'] ? ( + + + + ) : null} +
+
+ + + setFormItemValue((state: any) => { + return { ...state, 'refresh.groups.enabled': e }; + }) + } + /> + + {formItemValue['refresh.groups.enabled'] ? ( + + + + ) : null} +
+
+ + + setFormItemValue((state: any) => { + return { ...state, 'emit.checkpoints.enabled': e }; + }) + } + /> + + {formItemValue['emit.checkpoints.enabled'] ? ( +
+ + + + + + +
+ ) : null} +
+
+ + + +
+ {seniorConfig && ( +
setSeniorConfig(false)}> + 收起 + +
+ )} +
+
+ ); +}; + +// 步骤二 +const StepFormSecond = (props: SubFormProps) => { + const { clusterId } = useParams<{ + clusterId: string; + }>(); + const [form] = useStepForm(1); + const [firstForm] = useStepForm(0); + const connectClusterId = firstForm.getFieldValue('connectClusterId'); + const [activeKey, setActiveKey] = useState([]); + const { type, detail, errors, setHeartbeatDetailConfigs } = useContext(StepsFormContent); + const isEdit = type === 'edit'; + const [groupOffset, setGroupOffset] = useState(false); // 控制是否同步开关 + const [heartbeat, setHeartbeat] = useState(true); // 控制是否同步开关 + const [groupLoading, setGroupLoading] = useState(false); + const [groupBasicData, setGroupBasicData] = useState([]); + + const [formItemValues, setFormItemValues] = useState([]); + const [formItemValue, setFormItemValue] = useState({}); + const columns = [ + { + title: 'ConsumerGroup', + dataIndex: 'groupName', + key: 'groupName', + width: 200, + lineClampOne: true, + needTooltip: true, + }, + { + title: 'Topic', + dataIndex: 'topicName', + key: 'topicName', + width: 200, + lineClampOne: true, + needTooltip: true, + }, + { + title: '状态', + dataIndex: 'state', + key: 'state', + width: 60, + lineClampOne: true, + needTooltip: true, + }, + ]; + + const getGroupBasicData = () => { + setGroupLoading(true); + Utils.request(api.getGroupBasic(clusterId)) + .then((res: any) => { + setGroupBasicData(res || []); + }) + .finally(() => { + setGroupLoading(false); + }); + }; + + const getMM2Config = async (connectClusterId: string | number) => { + const formItemValue: any = {}; + const formItemValues: any = []; + const result: any = await Utils.request( + api.getConnectorPluginConfig(connectClusterId, 'org.apache.kafka.connect.mirror.MirrorHeartbeatConnector') + ); + const editResult: any = isEdit ? await Utils.request(api.getMirrorMakerConfig(connectClusterId, detail.connectorName)) : undefined; + let heartbeatConfigs: any; + editResult?.forEach((config: any) => { + if (config['connector.class'] === 'org.apache.kafka.connect.mirror.MirrorCheckpointConnector') { + heartbeatConfigs = config; + setHeartbeatDetailConfigs(config); + } + }); + result?.configs.forEach(({ definition }: any) => { + if (existConfigItems.heartbeatConfig.includes(definition.name)) { + if (isEdit && heartbeatConfigs[definition.name]) { + formItemValue[definition.name] = heartbeatConfigs[definition.name]; + formItemValues.push({ + name: definition.name, + value: heartbeatConfigs[definition.name] || null, + }); + } else { + formItemValue[definition.name] = definition.defaultValue; + formItemValues.push({ + name: definition.name, + value: definition.defaultValue || null, + }); + } + // formItemValue[definition.name] = definition.type === 'BOOLEAN' ? Boolean(definition.defaultValue) : definition.defaultValue || null; + // formItemValues.push({ + // name: definition.name, + // value: definition.type === 'BOOLEAN' ? Boolean(definition.defaultValue) : definition.defaultValue || null, + // }); + } + }); + if (formItemValue['heartbeats.topic.replication.factor'] || formItemValue['emit.heartbeats.interval.seconds']) { + formItemValue['heartbeat'] = true; + formItemValues.push({ + name: 'heartbeat', + value: true, + }); + } + setFormItemValue(formItemValue); + setFormItemValues(formItemValues); + }; + + useEffect(() => { + form.resetFields(existConfigItems.sourceConfigs); + form.setFields(formItemValues); + }, [formItemValues]); + + useEffect(() => { + connectClusterId + ? getMM2Config(connectClusterId) + : form.setFieldsValue({ + groupOffset: false, + heartbeat: true, + 'offset-syncs.topic.replication.factor': 3, + 'sync.group.offsets.interval.seconds': 60, + 'heartbeats.topic.replication.factor': 3, + 'emit.heartbeats.interval.seconds': 1, + }); + }, [connectClusterId]); + + useEffect(() => { + groupOffset && getGroupBasicData(); + }, [groupOffset]); + + return ( +
+
+ + setGroupOffset(e)} size="small" /> + + {groupOffset && ( +
+ + + + + + + + + )} + + + setFormItemValue((state: any) => { + return { ...state, heartbeat: e }; + }) + } + size="small" + /> + + {formItemValue['heartbeat'] ? ( +
+ + + + + + +
+ ) : null} + {/* {heartbeat && ( +
+ + + + + + +
+ )} */} + + + ); +}; + +const steps = [ + { + title: '步骤一', + content: StepFormFirst, + }, + { + title: '步骤二', + content: StepFormSecond, + }, +]; + +export default forwardRef( + ( + props: { + refresh: () => void; + }, + ref + ) => { + const [visible, setVisible] = useState(false); + const [jsonRef, setJsonRef] = useState({}); + const [currentStep, setCurrentStep] = useState(0); + const [stepInitState, setStepInitState] = useState([1]); + const [submitLoading, setSubmitLoading] = useState(false); + const [operateInfo, setOperateInfo] = useState({ + type: undefined, + errors: {}, + }); + const stepsFormRef = useRef<{ + [key: string]: FormInstance; + }>({}); + const [sourceKafkaClusterId, setSourceKafkaClusterId] = useState(''); + const [bootstrapServers, setBootstrapServers] = useState(null); + const [sourceDetailConfigs, setSourceDetailConfigs] = useState({}); + const [checkoutPointDetailConfigs, setCheckoutPointDetailConfigs] = useState({}); + const [heartbeatDetailConfigs, setHeartbeatDetailConfigs] = useState({}); + const onOpen = (type: OperateInfo['type'], jsonRef: any, detail?: OperateInfo['detail']) => { + if (type === 'create') { + setStepInitState([1]); + } else { + setStepInitState([1, 2, 3, 4]); + } + setOperateInfo({ + type, + detail, + errors: {}, + }); + setJsonRef(jsonRef); + setVisible(true); + }; + + const onClose = () => { + Object.values(stepsFormRef.current).forEach((form) => { + form.resetFields(); + }); + stepsFormRef.current = {}; + setVisible(false); + setCurrentStep(0); + setStepInitState([]); + }; + + const turnTo = (jumpStep: number) => { + if (submitLoading) { + message.warning('加载中,请稍后重试'); + return; + } + if (jumpStep > currentStep) { + const prevInit = stepInitState[jumpStep - 1]; + if (!prevInit) { + message.warning('请按照顺序填写'); + } else { + stepsFormRef.current[currentStep].validateFields().then(() => { + const prevStep = jumpStep - 1; + if (currentStep < prevStep) { + stepsFormRef.current[prevStep] + .validateFields() + .then(() => { + setStepInitState((prev) => { + const cur = [...prev]; + cur[jumpStep] = 1; + return cur; + }); + setCurrentStep(jumpStep); + }) + .catch(() => { + setCurrentStep(prevStep); + }); + } else { + setStepInitState((prev) => { + const cur = [...prev]; + cur[jumpStep] = 1; + return cur; + }); + setCurrentStep(jumpStep); + } + }); + } + } else { + setCurrentStep(jumpStep); + } + }; + + // 校验所有表单 + const validateForms = ( + callback: (info: { + success?: + | { + connectClusterId: number; + connectorName: string; + configs: { + [key: string]: any; + }; + sourceKafkaClusterId: number; + heartbeatConnectorConfigs: { + [key: string]: any; + }; + checkpointConnectorConfigs: { + [key: string]: any; + }; + } + | any; + error?: any; + }) => void + ) => { + const promises: Promise[] = []; + Object.values(stepsFormRef.current).forEach((form, i) => { + const promise = form + .validateFields() + .then((res) => { + return res; + }) + .catch(() => { + return Promise.reject(i); + }); + promises.push(promise); + }); + + Promise.all(promises).then( + (res) => { + const result = { + ...res[0], + ...res[1], + // ...res[4], + }; + const { detail } = operateInfo as any; + const checkpointConnectorConfigs = result['emit.checkpoints.enabled'] && { + ...checkoutPointDetailConfigs, + 'connector.class': 'org.apache.kafka.connect.mirror.MirrorCheckpointConnector', + 'emit.checkpoints.enabled': result['emit.checkpoints.enabled'], + 'emit.checkpoints.interval.seconds': result['emit.checkpoints.interval.seconds'], + 'checkpoints.topic.replication.factor': result['checkpoints.topic.replication.factor'], + 'source.cluster.alias': sourceKafkaClusterId, + name: detail?.checkpointConnector || result.name, + 'source.cluster.bootstrap.servers': bootstrapServers || checkoutPointDetailConfigs?.['source.cluster.bootstrap.servers'], + }; + const heartbeatConnectorConfigs = result['heartbeat'] && { + ...heartbeatDetailConfigs, + 'connector.class': 'org.apache.kafka.connect.mirror.MirrorHeartbeatConnector', + 'heartbeats.topic.replication.factor': result['heartbeats.topic.replication.factor'], + 'emit.heartbeats.interval.seconds': result['emit.heartbeats.interval.seconds'], + 'source.cluster.alias': sourceKafkaClusterId, + name: detail?.heartbeatConnector || result.name, + 'source.cluster.bootstrap.servers': bootstrapServers || heartbeatDetailConfigs?.['source.cluster.bootstrap.servers'], + }; + const configs = { + ...sourceDetailConfigs, + 'connector.class': 'org.apache.kafka.connect.mirror.MirrorSourceConnector', + 'sync.topic.configs.enabled': result['sync.topic.configs.enabled'], + 'sync.topic.configs.interval.seconds': result['sync.topic.configs.interval.seconds'], + + 'sync.topic.acls.enabled': result['sync.topic.acls.enabled'], + 'sync.topic.acls.interval.seconds': result['sync.topic.acls.interval.seconds'], + + 'refresh.topics.enabled': result['refresh.topics.enabled'], + 'refresh.topics.interval.seconds': result['refresh.topics.interval.seconds'], + + 'refresh.groups.enabled': result['refresh.groups.enabled'], + 'refresh.groups.interval.seconds': result['refresh.groups.interval.seconds'], + 'replication.policy.class': result['replication.policy.class'], + 'replication.policy.separator': result['replication.policy.separator'], + topics: result['priority'] === 'givenTopic' ? result['topics'].join() : '.*', + 'source.cluster.alias': sourceKafkaClusterId, + name: result.name, + 'source.cluster.bootstrap.servers': bootstrapServers || sourceDetailConfigs?.['source.cluster.bootstrap.servers'], + }; + // topics 配置格式化 + // res[1].topics && (result.topics = (res[1].topics as string[]).join(', ')); + // // transforms 配置格式化 + // res[2].transforms && + // (res[2].transforms as string) + // .split('\n') + // .filter((l) => l) + // .forEach((l) => { + // const [k, ...v] = l.split('='); + // result[k] = v.join('='); + // }); + callback({ + success: { + connectClusterId: res[0].connectClusterId, + connectorName: result['name'], + sourceKafkaClusterId: res[0].sourceKafkaClusterId, + configs, + heartbeatConnectorConfigs: heartbeatConnectorConfigs || undefined, + checkpointConnectorConfigs: checkpointConnectorConfigs || undefined, + }, + }); + }, + (error) => { + callback({ + error, + }); + } + ); + }; + + const toJsonMode = () => { + validateForms((info) => { + if (info.error) { + message.warning('校验失败,请检查填写内容'); + setCurrentStep(info.error); + } else { + (jsonRef as any)?.onOpen(operateInfo.type, info.success); + onClose(); + } + }); + }; + + const onSubmit = () => { + validateForms((info) => { + if (info.error) { + message.warning('校验失败,请检查填写内容'); + setCurrentStep(info.error); + } else { + setSubmitLoading(true); + Object.entries(info.success).forEach(([key, val]) => { + if (val === null) { + delete info.success[key]; + } + }); + Utils.put(api.validateMM2Config, info.success).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; + } + }); + setOperateInfo((cur) => ({ + ...cur, + errors, + })); + + // 步骤跳转 + // const items = getExistFormItems(stepsFormRef.current[0].getFieldValue('connectorType')); + // const keys = Object.keys(errors).filter((key) => items.includes(key)); + // let jumpStep = 4; + // keys.forEach((key) => { + // Object.values(existFormItems).some((items, i) => { + // if (items.includes(key)) { + // jumpStep > i + 1 && (jumpStep = i + 1); + // return true; + // } + // return false; + // }); + // }); + // setCurrentStep(jumpStep); + setSubmitLoading(false); + message.warning('字段校验失败,请检查'); + } else { + if (operateInfo.type === 'create') { + Utils.post(api.mirrorMakerOperates, info.success) + .then(() => { + message.success('新建成功'); + onClose(); + props?.refresh(); + }) + .finally(() => setSubmitLoading(false)); + } else { + Utils.put(api.updateMM2Config, info.success) + .then(() => { + message.success('编辑成功'); + props?.refresh(); + onClose(); + }) + .finally(() => setSubmitLoading(false)); + } + } + } else { + setSubmitLoading(false); + message.error('接口校验出错,请重新提交'); + } + }, + () => setSubmitLoading(false) + ); + } + }); + }; + + useImperativeHandle(ref, () => ({ + onOpen, + onClose, + })); + + return ( + + {operateInfo.type && visible && ( + <> + turnTo(cur)}> + {steps.map(({ title }) => ( + + ))} + +
+ + {steps.map((step, i) => { + return createElement(step.content, { + visible: i === currentStep, + setSubmitLoading, + }); + })} + + {currentStep === steps.length - 1 && ( + + 如果你想自定义更多配置,可以点击 + + 继续补充 + + } + /> + )} +
+ {currentStep > 0 && ( + + )} + {currentStep < steps.length - 1 && ( + + )} + {currentStep === steps.length - 1 && ( + + )} +
+
+ + )} +
+ ); + } +); diff --git a/km-console/packages/layout-clusters-fe/src/pages/MirrorMaker2/AddMM2JSON.tsx b/km-console/packages/layout-clusters-fe/src/pages/MirrorMaker2/AddMM2JSON.tsx new file mode 100644 index 00000000..35c6a2d6 --- /dev/null +++ b/km-console/packages/layout-clusters-fe/src/pages/MirrorMaker2/AddMM2JSON.tsx @@ -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 ( + + + + + + + + } + > +
+ { + // 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 && ( +
+ { + form.setFieldsValue({ configs }); + }} + /> +
+ )} +
+ +
+ ); +}); diff --git a/km-console/packages/layout-clusters-fe/src/pages/MirrorMaker2/Delete.tsx b/km-console/packages/layout-clusters-fe/src/pages/MirrorMaker2/Delete.tsx new file mode 100644 index 00000000..3ef93e6e --- /dev/null +++ b/km-console/packages/layout-clusters-fe/src/pages/MirrorMaker2/Delete.tsx @@ -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 ( + <> + + { + setDelDialogVisble(false); + }} + okText="删除" + okButtonProps={{ + danger: true, + size: 'small', + style: { + paddingLeft: '16px', + paddingRight: '16px', + }, + }} + cancelButtonProps={{ + size: 'small', + style: { + paddingLeft: '16px', + paddingRight: '16px', + }, + }} + > +
+ {record.connectorName} + ({ + 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(); + }, + }), + ]} + > + + + +
+ + ); +}; + +export default DeleteConnector; diff --git a/km-console/packages/layout-clusters-fe/src/pages/MirrorMaker2/Detail.tsx b/km-console/packages/layout-clusters-fe/src/pages/MirrorMaker2/Detail.tsx new file mode 100644 index 00000000..a5b12902 --- /dev/null +++ b/km-console/packages/layout-clusters-fe/src/pages/MirrorMaker2/Detail.tsx @@ -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(defaultPagination); + const onTableChange = (pagination: any, filters: any, sorter: any) => { + setPagination(pagination); + }; + return ( + + {data.length ? ( + + ) : ( + + )} + + ); +}; + +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('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 ( + + {record.connectorName ?? '-'} + + } + width={1080} + placement="right" + onClose={onClose} + visible={visible} + className={`${prefix}-drawer`} + destroyOnClose + maskClosable={false} + > + + + + + {/* {global.isShowControl && global.isShowControl(ControlStatusMap.BROKER_DETAIL_CONFIG) ? ( + + ) : ( + + )} */} + + + + + + + + + {/* */} + + ); +}; + +export default MM2Detail; diff --git a/km-console/packages/layout-clusters-fe/src/pages/MirrorMaker2/config.tsx b/km-console/packages/layout-clusters-fe/src/pages/MirrorMaker2/config.tsx new file mode 100644 index 00000000..b0e21d3b --- /dev/null +++ b/km-console/packages/layout-clusters-fe/src/pages/MirrorMaker2/config.tsx @@ -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 ? ( +
+ ({ time: item.timeStamp, value: item.value })), + }} + /> + {calcCurValue(record, metricName)} +
+ ) : ( + {calcCurValue(record, metricName)} + ); +}; + +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 ? ( + <> + + { + arg.getDetailInfo(r); + }} + > + {t} + + + + ) : ( + '-' + ); + }, + }, + { + 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 ? ( + + {stateEnum[t]?.name} + + ) : ( + '-' + ); + }, + }, + { + // title: '集群(源-->目标)', + title: ( + + 集群(源{' '} + + + {' '} + 目标) + + ), + dataIndex: 'destKafkaClusterName', + key: 'destKafkaClusterName', + width: 200, + render: (t: string, r: any) => { + return r.sourceKafkaClusterName && r.destKafkaClusterName ? ( + + {r.sourceKafkaClusterName} + + {t} + + ) : ( + '-' + ); + }, + }, + { + 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 ( +
+ {global.hasPermission(ClustersPermissionMap.MM2_STOP_RESUME) && (r.state === 'RUNNING' || r.state === 'PAUSED') && ( + arg?.optionConnect(r, r.state === 'RUNNING' ? 'stop' : 'resume')} + // onCancel={cancel} + okText="是" + cancelText="否" + overlayClassName="connect-popconfirm" + > + + + )} + {global.hasPermission(ClustersPermissionMap.MM2_RESTART) ? ( + arg?.optionConnect(r, 'restart')} + // onCancel={cancel} + okText="是" + cancelText="否" + overlayClassName="connect-popconfirm" + > + + + ) : ( + <> + )} + {global.hasPermission(ClustersPermissionMap.MM2_CHANGE_CONFIG) ? ( + r.sourceKafkaClusterId ? ( + + ) : ( + + + + ) + ) : ( + <> + )} + {global.hasPermission(ClustersPermissionMap.MM2_DELETE) ? : <>} +
+ ); + }, + }); + } + 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 ( + + {t} + { + + {Utils.firstCharUppercase(r?.state as string)} + + } + + ); + }, + }, + { + 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 ( +
+ arg?.retryOption(r.taskId)} + // onCancel={cancel} + okText="是" + cancelText="否" + overlayClassName="connect-popconfirm" + > + 重试 + +
+ ); + }, + }, + ]; + return columns; +}; diff --git a/km-console/packages/layout-clusters-fe/src/pages/MirrorMaker2/index.less b/km-console/packages/layout-clusters-fe/src/pages/MirrorMaker2/index.less new file mode 100644 index 00000000..82664902 --- /dev/null +++ b/km-console/packages/layout-clusters-fe/src/pages/MirrorMaker2/index.less @@ -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; + } + } +} diff --git a/km-console/packages/layout-clusters-fe/src/pages/MirrorMaker2/index.tsx b/km-console/packages/layout-clusters-fe/src/pages/MirrorMaker2/index.tsx new file mode 100644 index 00000000..42f42694 --- /dev/null +++ b/km-console/packages/layout-clusters-fe/src/pages/MirrorMaker2/index.tsx @@ -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(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 = ( + + + addConnectorJsonRef.current?.onOpen('create')}>JSON 新增MM2 + + + ); + + 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 ( + <> +
+ +
+ {/* + <> + + + */} +
+ +
+
+
+
+
genData({ pageNo: pagination.current, pageSize: pagination.pageSize })} + > + +
+
+
+ + {global.hasPermission && global.hasPermission(ClustersPermissionMap.MM2_ADD) ? ( + + + + + + + ) : ( + <> + )} +
+
+ +
+ + genData({ pageNo: pagination.current, pageSize: pagination.pageSize, sorter: sortInfo })} + /> + genData({ pageNo: pagination.current, pageSize: pagination.pageSize, sorter: sortInfo })} + /> + + ); +}; + +export default MirrorMaker2; diff --git a/km-console/packages/layout-clusters-fe/src/pages/pageRoutes.ts b/km-console/packages/layout-clusters-fe/src/pages/pageRoutes.ts index a781c948..cc69935d 100644 --- a/km-console/packages/layout-clusters-fe/src/pages/pageRoutes.ts +++ b/km-console/packages/layout-clusters-fe/src/pages/pageRoutes.ts @@ -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,