From a0371ab88b26de2803ee1ee3cc98e29cc2712178 Mon Sep 17 00:00:00 2001 From: wyb Date: Fri, 10 Feb 2023 16:56:13 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20=E6=96=B0=E5=A2=9ETopic=20=E5=A4=8D?= =?UTF-8?q?=E5=88=B6=E5=8A=9F=E8=83=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../src/components/TopicJob/TopicMirror.tsx | 289 ++++++++++++++++++ .../src/pages/TopicDetail/Replicator.tsx | 173 +++++++++++ .../src/pages/TopicDetail/index.tsx | 4 + .../src/pages/TopicList/index.tsx | 53 +++- 4 files changed, 507 insertions(+), 12 deletions(-) create mode 100644 km-console/packages/layout-clusters-fe/src/components/TopicJob/TopicMirror.tsx create mode 100644 km-console/packages/layout-clusters-fe/src/pages/TopicDetail/Replicator.tsx diff --git a/km-console/packages/layout-clusters-fe/src/components/TopicJob/TopicMirror.tsx b/km-console/packages/layout-clusters-fe/src/components/TopicJob/TopicMirror.tsx new file mode 100644 index 00000000..2845dcae --- /dev/null +++ b/km-console/packages/layout-clusters-fe/src/components/TopicJob/TopicMirror.tsx @@ -0,0 +1,289 @@ +// 批量Topic复制 +import React, { useState, useEffect } from 'react'; +import { useParams } from 'react-router-dom'; +import { Button, Drawer, Form, Select, Utils, AppContainer, Space, Divider, Transfer, Checkbox, Tooltip } from 'knowdesign'; +import message from '@src/components/Message'; +import { IconFont } from '@knowdesign/icons'; +import { QuestionCircleOutlined } from '@ant-design/icons'; +import './index.less'; +import Api from '@src/api/index'; + +const { Option } = Select; +const CheckboxGroup = Checkbox.Group; + +interface DefaultConfig { + drawerVisible: boolean; + onClose: () => void; + genData?: () => any; +} + +export default (props: DefaultConfig) => { + const { drawerVisible, onClose, genData } = props; + const routeParams = useParams<{ clusterId: string }>(); + const [visible, setVisible] = useState(drawerVisible); + const [topicList, setTopicList] = useState([]); + const [clusterList, setClusterList] = useState([]); + const [selectTopicList, setSelectTopicList] = useState([]); + const [form] = Form.useForm(); + const topicsPerCluster = React.useRef({} as any); + + const mirrorScopeOptions = [ + { + label: '数据', + value: 'syncData', + }, + { + label: 'Topic配置', + value: 'syncConfig', + }, + { + label: 'kafkauser+Acls', + value: 'kafkauserAcls', + disabled: true, + }, + { + label: 'Group Offset', + value: 'groupOffset', + disabled: true, + }, + ]; + + const getTopicList = () => { + Utils.request(Api.getTopicMetaData(+routeParams.clusterId)).then((res: any) => { + const dataDe = res || []; + const dataHandle = dataDe.map((item: any) => { + return { + ...item, + key: item.topicName, + title: item.topicName, + }; + }); + setTopicList(dataHandle); + }); + }; + + const getTopicsPerCluster = (clusterId: number) => { + Utils.request(Api.getTopicMetaData(clusterId)).then((res: any) => { + const dataDe = res || []; + const dataHandle = dataDe.map((item: any) => item.topicName); + topicsPerCluster.current = { ...topicsPerCluster.current, [clusterId]: dataHandle }; + setTimeout(() => { + form.validateFields(['topicNames']); + }, 1000); + }); + }; + + const getClusterList = () => { + Utils.request(Api.getMirrorClusterList()).then((res: any) => { + const dataDe = res || []; + const dataHandle = dataDe.map((item: any) => { + return { + label: item.name, + value: item.id, + }; + }); + setClusterList(dataHandle); + }); + }; + + const checkTopic = (_: any, value: any[]) => { + const clusters = form.getFieldValue('destClusterPhyIds'); + if (!value || !value.length) { + return Promise.reject('请选择需要复制的Topic'); + } else { + if (clusters && clusters.length) { + // 验证Topic是否存在 + const existTopics = {} as any; + clusters.forEach((cluster: number) => { + if (cluster && topicsPerCluster.current[cluster]) { + existTopics[cluster] = []; + value.forEach((topic) => { + if (topicsPerCluster.current[cluster].indexOf(topic) > -1) { + existTopics[cluster].push(topic); + } + }); + if (!existTopics[cluster].length) delete existTopics[cluster]; + } else { + getTopicsPerCluster(cluster); + } + }); + if (Object.keys(existTopics).length) { + let errorInfo = ''; + Object.keys(existTopics).forEach((key) => { + const clusterName = clusterList.find((item) => item.value == key)?.label; + errorInfo = errorInfo.concat(`${existTopics[key].join('、')}在集群【${clusterName}】中已存在,`); + }); + errorInfo = errorInfo.concat('请重新选择'); + return Promise.reject(errorInfo); + } else { + return Promise.resolve(); + } + } else { + return Promise.resolve(); + } + } + }; + + const topicChange = (val: string[]) => { + setSelectTopicList(val); + }; + + const clusterChange = (val: number[]) => { + if (val && val.length) { + val.forEach((item) => { + if (item && !topicsPerCluster.current[item]) { + getTopicsPerCluster(item); + } else { + form.validateFields(['topicNames']); + } + }); + } else { + form.validateFields(['topicNames']); + } + }; + + const onDrawerClose = () => { + form.resetFields(); + setSelectTopicList([]); + topicsPerCluster.current = {}; + setVisible(false); + onClose(); + }; + + useEffect(() => { + if (!drawerVisible) return; + setVisible(true); + getTopicList(); + getClusterList(); + }, [drawerVisible]); + + const addTopicMirror = () => { + form.validateFields().then((e) => { + const formData = form.getFieldsValue(); + const handledData = [] as any; + formData.destClusterPhyIds.forEach((cluster: number) => { + formData.topicNames.forEach((topic: string) => { + handledData.push({ + destClusterPhyId: cluster, + sourceClusterPhyId: +routeParams.clusterId, + syncData: formData.mirrorScope.indexOf('syncData') > -1, + syncConfig: formData.mirrorScope.indexOf('syncConfig') > -1, + topicName: topic, + }); + }); + }); + Utils.post(Api.handleTopicMirror(), handledData).then(() => { + message.success('成功复制Topic'); + onDrawerClose(); + genData(); + }); + }); + }; + + return ( + + + + + + } + > +
+
+ + option.topicName.indexOf(inputValue) > -1} + targetKeys={selectTopicList} + onChange={topicChange} + render={(item) => item.title} + titles={['待选Topic', '已选Topic']} + customHeader + showSelectedCount + locale={{ itemUnit: '', itemsUnit: '' }} + suffix={} + /> + + + 选择目标集群 + + + + + } + rules={[{ required: true, message: '请选择目标集群' }]} + > + + + { + if (!value || !value.length) { + return Promise.reject('请选择Topic复制范围'); + } else if (value.indexOf('syncData') === -1) { + return Promise.reject('Topic复制范围必须选择[数据]'); + } else { + return Promise.resolve(); + } + }, + }, + ]} + initialValue={['syncData']} + > + + {mirrorScopeOptions.map((option) => { + return option.disabled ? ( + + + {option.label} + + + ) : ( + + {option.label} + + ); + })} + + +
+
+
+ ); +}; diff --git a/km-console/packages/layout-clusters-fe/src/pages/TopicDetail/Replicator.tsx b/km-console/packages/layout-clusters-fe/src/pages/TopicDetail/Replicator.tsx new file mode 100644 index 00000000..89e27479 --- /dev/null +++ b/km-console/packages/layout-clusters-fe/src/pages/TopicDetail/Replicator.tsx @@ -0,0 +1,173 @@ +import React, { useState, useEffect } from 'react'; +import { AppContainer, ProTable, Utils, Tag, Modal, Tooltip } from 'knowdesign'; +import Api from '@src/api'; +import { useParams } from 'react-router-dom'; +import { getDataUnit } from '@src/constants/chartConfig'; +import message from '@src/components/Message'; +import { ClustersPermissionMap } from '../CommonConfig'; +import { ControlStatusMap } from '../CommonRoute'; +const { request } = Utils; + +const getColmns = (arg: any) => { + const formattedBytes = (v: number) => { + const [unit, size] = getDataUnit['Memory'](v); + return `${(v / size).toFixed(2)}${unit}/s`; + }; + const tagEle = ( + + 当前集群 + + ); + const baseColumns: any = [ + { + title: '源集群', + dataIndex: 'sourceClusterName', + key: 'sourceClusterName', + render: (t: string, record: any) => ( + <> + {t || '-'} + {record.sourceClusterId == arg.clusterId && tagEle} + + ), + }, + { + title: '目标集群', + dataIndex: 'destClusterName', + key: 'destClusterName', + render: (t: string, record: any) => ( + <> + {t || '-'} + {record.destClusterId == arg.clusterId && tagEle} + + ), + }, + { + title: '消息写入速率', + dataIndex: 'bytesIn', + key: 'bytesIn', + width: 150, + render: (t: number) => (t !== null && t !== undefined ? formattedBytes(t) : '-'), + }, + { + title: '消息复制速率', + dataIndex: 'replicationBytesIn', + key: 'replicationBytesIn', + width: 150, + render: (t: number) => (t !== null && t !== undefined ? formattedBytes(t) : '-'), + }, + { + title: '延迟(个消息)', + dataIndex: 'lag', + key: 'lag', + width: 150, + }, + { + title: '操作', + dataIndex: 'option', + key: 'option', + width: 100, + render: (_t: any, r: any) => { + return arg.global.hasPermission(ClustersPermissionMap.TOPIC_CANCEL_REPLICATOR) ? ( + arg.cancelSync(r)}>取消同步 + ) : ( + '-' + ); + }, + }, + ]; + + return baseColumns; +}; + +const Replicator = (props: any) => { + const { hashData } = props; + const urlParams = useParams(); // 获取地址栏参数 + const [global] = AppContainer.useGlobalValue(); + const [loading, setLoading] = useState(false); + const [data, setData] = useState([]); + const [pagination, setPagination] = useState({ + current: 1, + pageSize: 10, + position: 'bottomRight', + showSizeChanger: true, + pageSizeOptions: ['10', '20', '50', '100', '200', '500'], + }); + + const genData = () => { + if (urlParams?.clusterId === undefined || hashData?.topicName === undefined) return; + setLoading(true); + request(Api.getTopicMirrorList(urlParams?.clusterId, hashData?.topicName)) + .then((res: any = []) => { + setData(res); + }) + .finally(() => setLoading(false)); + }; + + const cancelSync = (item: any) => { + Modal.confirm({ + title: `确认取消此Topic同步吗?`, + okType: 'primary', + centered: true, + okButtonProps: { + size: 'small', + danger: true, + }, + cancelButtonProps: { + size: 'small', + }, + maskClosable: false, + onOk(close) { + close(); + const data = [ + { + destClusterPhyId: item.destClusterId, + sourceClusterPhyId: item.sourceClusterId, + topicName: item.topicName, + }, + ]; + Utils.delete(Api.handleTopicMirror(), { data }).then(() => { + message.success('成功取消Topic同步'); + genData(); + }); + }, + }); + }; + + const onTableChange = (pagination: any, filters: any, sorter: any, extra: any) => { + setPagination(pagination); + }; + + useEffect(() => { + props.positionType === 'Replicator' && genData(); + }, []); + + return ( +
+ +
+ ); +}; + +export default Replicator; diff --git a/km-console/packages/layout-clusters-fe/src/pages/TopicDetail/index.tsx b/km-console/packages/layout-clusters-fe/src/pages/TopicDetail/index.tsx index bc3f6ed4..ea009bd4 100644 --- a/km-console/packages/layout-clusters-fe/src/pages/TopicDetail/index.tsx +++ b/km-console/packages/layout-clusters-fe/src/pages/TopicDetail/index.tsx @@ -10,6 +10,7 @@ import ConsumerGroups from './ConsumerGroups'; import ACLs from './ACLs'; import Configuration from './Configuration'; import Consumers from './ConsumerGroups'; +import Replicator from './Replicator'; // import Consumers from '@src/pages/Consumers'; import './index.less'; import TopicDetailHealthCheck from '@src/components/CardBar/TopicDetailHealthCheck'; @@ -206,6 +207,9 @@ const TopicDetail = (props: any) => { )} + + {positionType === 'Replicator' && } + ); diff --git a/km-console/packages/layout-clusters-fe/src/pages/TopicList/index.tsx b/km-console/packages/layout-clusters-fe/src/pages/TopicList/index.tsx index 8e35af9a..4c3f8857 100644 --- a/km-console/packages/layout-clusters-fe/src/pages/TopicList/index.tsx +++ b/km-console/packages/layout-clusters-fe/src/pages/TopicList/index.tsx @@ -1,7 +1,7 @@ /* eslint-disable react/display-name */ import React, { useState, useEffect } from 'react'; import { useHistory, useParams } from 'react-router-dom'; -import { AppContainer, Input, ProTable, Select, Switch, Tooltip, Utils, Dropdown, Menu, Button, Divider } from 'knowdesign'; +import { AppContainer, Input, ProTable, Select, Switch, Tooltip, Utils, Dropdown, Menu, Button, Divider, Tag } from 'knowdesign'; import { IconFont } from '@knowdesign/icons'; import Create from './Create'; import './index.less'; @@ -15,10 +15,12 @@ import DBreadcrumb from 'knowdesign/es/extend/d-breadcrumb'; import ReplicaChange from '@src/components/TopicJob/ReplicaChange'; import SmallChart from '@src/components/SmallChart'; import ReplicaMove from '@src/components/TopicJob/ReplicaMove'; +import TopicMirror from '@src/components/TopicJob/TopicMirror'; import { formatAssignSize } from '../Jobs/config'; import { DownOutlined } from '@ant-design/icons'; import { tableHeaderPrefix } from '@src/constants/common'; import { HealthStateMap } from './config'; +import { ControlStatusMap } from '../CommonRoute'; const { Option } = Select; @@ -39,6 +41,7 @@ const AutoPage = (props: any) => { const [type, setType] = useState(''); const [changeVisible, setChangeVisible] = useState(false); const [moveVisible, setMoveVisible] = useState(false); + const [mirrorVisible, setMirrorVisible] = useState(false); const [selectValue, setSelectValue] = useState('批量操作'); const [sortObj, setSortObj] = useState<{ @@ -131,17 +134,34 @@ const AutoPage = (props: any) => { className: 'clean-padding-left', lineClampOne: true, // eslint-disable-next-line react/display-name - render: (t: string, r: any) => { + render: (t: string, record: any) => { return ( - - { - window.location.hash = `topicName=${t}`; - }} - > - {t} - - + <> + + { + window.location.hash = `topicName=${t}`; + }} + > + {t} + + + {record.inMirror && ( +
+ + 复制中... + +
+ )} + ); }, }, @@ -256,6 +276,7 @@ const AutoPage = (props: any) => { const onclose = () => { setChangeVisible(false); setMoveVisible(false); + setMirrorVisible(false); setSelectValue('批量操作'); }; @@ -271,6 +292,11 @@ const AutoPage = (props: any) => { setMoveVisible(true)}>迁移副本 )} + {global.hasPermission(ClustersPermissionMap.TOPIC_REPLICATOR) && ( + + setMirrorVisible(true)}>Topic复制 + + )} ); @@ -296,6 +322,8 @@ const AutoPage = (props: any) => { {/* 批量迁移 */} + {/* Topic复制 */} +
getTopicsList()}> @@ -334,7 +362,8 @@ const AutoPage = (props: any) => { }} /> {(global.hasPermission(ClustersPermissionMap.TOPIC_CHANGE_REPLICA) || - global.hasPermission(ClustersPermissionMap.TOPIC_MOVE_REPLICA)) && ( + global.hasPermission(ClustersPermissionMap.TOPIC_MOVE_REPLICA) || + global.hasPermission(ClustersPermissionMap.TOPIC_REPLICATOR)) && (