mirror of
https://github.com/didi/KnowStreaming.git
synced 2026-01-03 19:38:20 +08:00
feat: 新增Mirror Maker 2.0(MM2)
This commit is contained in:
@@ -26,11 +26,19 @@ export enum ClustersPermissionMap {
|
||||
TOPIC_ADD = 'Topic-新增Topic',
|
||||
TOPIC_MOVE_REPLICA = 'Topic-迁移副本',
|
||||
TOPIC_CHANGE_REPLICA = 'Topic-扩缩副本',
|
||||
TOPIC_REPLICATOR = 'Topic-新增Topic复制',
|
||||
TOPIC_CANCEL_REPLICATOR = 'Topic-详情-取消Topic复制',
|
||||
// Consumers
|
||||
CONSUMERS_RESET_OFFSET = 'Consumers-重置Offset',
|
||||
// Test
|
||||
TEST_CONSUMER = 'Test-Consumer',
|
||||
TEST_PRODUCER = 'Test-Producer',
|
||||
// MM2
|
||||
MM2_ADD = 'MM2-新增',
|
||||
MM2_CHANGE_CONFIG = 'MM2-编辑',
|
||||
MM2_DELETE = 'MM2-删除',
|
||||
MM2_RESTART = 'MM2-重启',
|
||||
MM2_STOP_RESUME = 'MM2-暂停&恢复',
|
||||
}
|
||||
|
||||
export interface PermissionNode {
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,266 @@
|
||||
import api from '@src/api';
|
||||
import CodeMirrorFormItem from '@src/components/CodeMirrorFormItem';
|
||||
import customMessage from '@src/components/Message';
|
||||
import { Button, Divider, Drawer, Form, message, Space, Utils } from 'knowdesign';
|
||||
import React, { forwardRef, useEffect, useImperativeHandle, useState } from 'react';
|
||||
import { useParams } from 'react-router-dom';
|
||||
import { ConnectCluster, ConnectorPlugin, ConnectorPluginConfig, OperateInfo } from './AddMM2';
|
||||
|
||||
const PLACEHOLDER = `配置格式如下
|
||||
|
||||
{
|
||||
"connectClusterId": 1, // ConnectID
|
||||
"connectorName": "", // MM2 名称
|
||||
"sourceKafkaClusterId": 1, // SourceKafka集群 ID
|
||||
"configs": { // Source 相关配置
|
||||
"name": "", // MM2 名称
|
||||
"source.cluster.alias": "", // SourceKafka集群 ID
|
||||
...
|
||||
},
|
||||
"heartbeatConnectorConfigs": { // Heartbeat 相关配置
|
||||
"name": "", // Heartbeat 对应的Connector名称
|
||||
"source.cluster.alias": "", // SourceKafka集群 ID
|
||||
...
|
||||
},
|
||||
"checkpointConnectorConfigs": { // Checkpoint 相关配置
|
||||
"name": "", // Checkpoint 对应的Connector名称
|
||||
"source.cluster.alias": "", // SourceKafka集群 ID
|
||||
...
|
||||
}
|
||||
}`;
|
||||
|
||||
export default forwardRef((props: any, ref) => {
|
||||
// const { clusterId } = useParams<{
|
||||
// clusterId: string;
|
||||
// }>();
|
||||
const [visible, setVisible] = useState(false);
|
||||
const [form] = Form.useForm();
|
||||
const [type, setType] = useState('create');
|
||||
// const [connectClusters, setConnectClusters] = useState<{ label: string; value: number }[]>([]);
|
||||
const [defaultConfigs, setDefaultConfigs] = useState<{ [key: string]: any }>({});
|
||||
const [submitLoading, setSubmitLoading] = useState(false);
|
||||
|
||||
// const getConnectClusters = () => {
|
||||
// return Utils.request(api.getConnectClusters(clusterId)).then((res: ConnectCluster[]) => {
|
||||
// setConnectClusters(
|
||||
// res.map(({ name, id }) => ({
|
||||
// label: name || '-',
|
||||
// value: id,
|
||||
// }))
|
||||
// );
|
||||
// });
|
||||
// };
|
||||
|
||||
const onOpen = (type: 'create' | 'edit', defaultConfigs?: { [key: string]: any }) => {
|
||||
if (defaultConfigs) {
|
||||
setDefaultConfigs({ ...defaultConfigs });
|
||||
form.setFieldsValue({
|
||||
configs: JSON.stringify(defaultConfigs, null, 2),
|
||||
});
|
||||
}
|
||||
setType(type);
|
||||
setVisible(true);
|
||||
};
|
||||
|
||||
const onSubmit = () => {
|
||||
setSubmitLoading(true);
|
||||
form.validateFields().then(
|
||||
(data) => {
|
||||
const postData = JSON.parse(data.configs);
|
||||
|
||||
Object.entries(postData.configs).forEach(([key, val]) => {
|
||||
if (val === null) {
|
||||
delete postData.configs[key];
|
||||
}
|
||||
});
|
||||
Utils.put(api.validateMM2Config, postData).then(
|
||||
(res: ConnectorPluginConfig) => {
|
||||
if (res) {
|
||||
if (res?.errorCount > 0) {
|
||||
const errors: OperateInfo['errors'] = {};
|
||||
res?.configs
|
||||
?.filter((config) => config.value.errors.length !== 0)
|
||||
.forEach(({ value }) => {
|
||||
if (value.name.includes('transforms.')) {
|
||||
errors['transforms'] = (errors['transforms'] || []).concat(value.errors);
|
||||
} else {
|
||||
errors[value.name] = value.errors;
|
||||
}
|
||||
});
|
||||
form.setFields([
|
||||
{
|
||||
name: 'configs',
|
||||
errors: Object.entries(errors).map(([name, errorArr]) => `${name}: ${errorArr.join('; ')}\n`),
|
||||
},
|
||||
]);
|
||||
setSubmitLoading(false);
|
||||
} else {
|
||||
if (type === 'create') {
|
||||
Utils.post(api.mirrorMakerOperates, postData)
|
||||
.then(() => {
|
||||
customMessage.success('新建成功');
|
||||
onClose();
|
||||
props?.refresh();
|
||||
})
|
||||
.finally(() => setSubmitLoading(false));
|
||||
} else {
|
||||
Utils.put(api.updateMM2Config, postData)
|
||||
.then(() => {
|
||||
customMessage.success('编辑成功');
|
||||
props?.refresh();
|
||||
onClose();
|
||||
})
|
||||
.finally(() => setSubmitLoading(false));
|
||||
}
|
||||
}
|
||||
} else {
|
||||
setSubmitLoading(false);
|
||||
message.error('接口校验出错,请重新提交');
|
||||
}
|
||||
},
|
||||
() => setSubmitLoading(false)
|
||||
);
|
||||
},
|
||||
() => setSubmitLoading(false)
|
||||
);
|
||||
};
|
||||
|
||||
const onClose = () => {
|
||||
setVisible(false);
|
||||
form.resetFields();
|
||||
};
|
||||
|
||||
// useEffect(() => {
|
||||
// getConnectClusters();
|
||||
// }, []);
|
||||
|
||||
useImperativeHandle(ref, () => ({
|
||||
onOpen,
|
||||
onClose,
|
||||
}));
|
||||
|
||||
return (
|
||||
<Drawer
|
||||
title={`${type === 'create' ? '新建' : '编辑'} MM2`}
|
||||
className="operate-connector-drawer-use-json"
|
||||
width={800}
|
||||
visible={visible}
|
||||
onClose={onClose}
|
||||
maskClosable={false}
|
||||
extra={
|
||||
<div className="operate-wrap">
|
||||
<Space>
|
||||
<Button size="small" onClick={onClose}>
|
||||
取消
|
||||
</Button>
|
||||
<Button size="small" type="primary" onClick={onSubmit} loading={submitLoading}>
|
||||
确定
|
||||
</Button>
|
||||
<Divider type="vertical" />
|
||||
</Space>
|
||||
</div>
|
||||
}
|
||||
>
|
||||
<Form form={form} layout="vertical">
|
||||
<Form.Item
|
||||
name="configs"
|
||||
validateTrigger="onBlur"
|
||||
rules={[
|
||||
{
|
||||
validator(rule, value) {
|
||||
if (!value) {
|
||||
return Promise.reject('配置不能为空');
|
||||
}
|
||||
try {
|
||||
const v = JSON.parse(value);
|
||||
if (typeof v !== 'object') {
|
||||
return Promise.reject('输入内容必须为 JSON');
|
||||
}
|
||||
// let connectClusterId = -1;
|
||||
// ! 校验 connectorName 字段
|
||||
if (!v.connectorName) {
|
||||
return Promise.reject('内容缺少 MM2任务名称 字段或字段内容为空');
|
||||
} else {
|
||||
if (type === 'edit') {
|
||||
if (v.connectorName !== defaultConfigs.connectorName) {
|
||||
return Promise.reject('编辑模式下不允许修改 MM2任务名称 字段');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ! 校验connectClusterId
|
||||
if (!v.connectClusterId) {
|
||||
return Promise.reject('内容缺少 connectClusterId 字段或字段内容为空');
|
||||
}
|
||||
// ! 校验sourceKafkaClusterId
|
||||
if (!v.sourceKafkaClusterId) {
|
||||
return Promise.reject('内容缺少 sourceKafkaClusterId 字段或字段内容为空');
|
||||
}
|
||||
// ! 校验configs
|
||||
if (!v.configs || typeof v.configs !== 'object') {
|
||||
return Promise.reject('内容缺少 configs 字段或字段格式错误');
|
||||
} else {
|
||||
// ! 校验Topic
|
||||
if (!v.configs.topics) {
|
||||
return Promise.reject('configs 字段下缺少 topics 项');
|
||||
}
|
||||
|
||||
// 校验 connectorName 字段
|
||||
// if (!v.configs.name) {
|
||||
// return Promise.reject('configs 字段下缺少 name 项');
|
||||
// } else {
|
||||
// if (type === 'edit' && v.configs.name !== defaultConfigs.name) {
|
||||
// return Promise.reject('编辑模式下不允许修改 name 字段');
|
||||
// }
|
||||
// }
|
||||
// if (!v.configs['connector.class']) {
|
||||
// return Promise.reject('configs 字段下缺少 connector.class 项');
|
||||
// } else if (type === 'edit' && v.configs['connector.class'] !== defaultConfigs['connector.class']) {
|
||||
// return Promise.reject('编辑模式下不允许修改 connector.class 字段');
|
||||
// }
|
||||
}
|
||||
return Promise.resolve();
|
||||
// if (type === 'create') {
|
||||
// // 异步校验 connector 名称是否重复 以及 className 是否存在
|
||||
// return Promise.all([
|
||||
// Utils.request(api.isConnectorExist(connectClusterId, v.configs.name)),
|
||||
// Utils.request(api.getConnectorPlugins(connectClusterId)),
|
||||
// ]).then(
|
||||
// ([data, plugins]: [any, ConnectorPlugin[]]) => {
|
||||
// return data?.exist
|
||||
// ? Promise.reject('name 与已有 Connector 重复')
|
||||
// : plugins.every((plugin) => plugin.className !== v.configs['connector.class'])
|
||||
// ? Promise.reject('该 connectCluster 下不存在 connector.class 项配置的插件')
|
||||
// : Promise.resolve();
|
||||
// },
|
||||
// () => {
|
||||
// return Promise.reject('接口校验出错,请重试');
|
||||
// }
|
||||
// );
|
||||
// } else {
|
||||
// return Promise.resolve();
|
||||
// }
|
||||
} catch (e) {
|
||||
return Promise.reject('输入内容必须为 JSON');
|
||||
}
|
||||
},
|
||||
},
|
||||
]}
|
||||
>
|
||||
{visible && (
|
||||
<div>
|
||||
<CodeMirrorFormItem
|
||||
resize
|
||||
defaultInput={form.getFieldValue('configs')}
|
||||
placeholder={PLACEHOLDER}
|
||||
onBeforeChange={(configs: string) => {
|
||||
form.setFieldsValue({ configs });
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
</Form.Item>
|
||||
</Form>
|
||||
</Drawer>
|
||||
);
|
||||
});
|
||||
@@ -0,0 +1,99 @@
|
||||
import React, { useState } from 'react';
|
||||
import { useParams } from 'react-router-dom';
|
||||
import { Button, Form, Input, Modal, Utils } from 'knowdesign';
|
||||
import notification from '@src/components/Notification';
|
||||
import { IconFont } from '@knowdesign/icons';
|
||||
import Api from '@src/api/index';
|
||||
|
||||
// eslint-disable-next-line react/display-name
|
||||
const DeleteConnector = (props: { record: any; onConfirm?: () => void }) => {
|
||||
const { record, onConfirm } = props;
|
||||
const [form] = Form.useForm();
|
||||
const [delDialogVisible, setDelDialogVisble] = useState(false);
|
||||
const handleDelOk = () => {
|
||||
form.validateFields().then((e) => {
|
||||
const formVal = form.getFieldsValue();
|
||||
formVal.connectClusterId = Number(record.connectClusterId);
|
||||
Utils.delete(Api.mirrorMakerOperates, { data: formVal }).then((res: any) => {
|
||||
if (res === null) {
|
||||
notification.success({
|
||||
message: '删除成功',
|
||||
});
|
||||
setDelDialogVisble(false);
|
||||
onConfirm && onConfirm();
|
||||
} else {
|
||||
notification.error({
|
||||
message: '删除失败',
|
||||
});
|
||||
}
|
||||
});
|
||||
});
|
||||
};
|
||||
return (
|
||||
<>
|
||||
<Button
|
||||
type="link"
|
||||
size="small"
|
||||
onClick={(_) => {
|
||||
setDelDialogVisble(true);
|
||||
}}
|
||||
>
|
||||
删除
|
||||
</Button>
|
||||
<Modal
|
||||
className="custom-modal"
|
||||
title="确定删除此 MM2 任务吗?"
|
||||
centered={true}
|
||||
visible={delDialogVisible}
|
||||
wrapClassName="del-connect-modal"
|
||||
destroyOnClose={true}
|
||||
maskClosable={false}
|
||||
onOk={handleDelOk}
|
||||
onCancel={(_) => {
|
||||
setDelDialogVisble(false);
|
||||
}}
|
||||
okText="删除"
|
||||
okButtonProps={{
|
||||
danger: true,
|
||||
size: 'small',
|
||||
style: {
|
||||
paddingLeft: '16px',
|
||||
paddingRight: '16px',
|
||||
},
|
||||
}}
|
||||
cancelButtonProps={{
|
||||
size: 'small',
|
||||
style: {
|
||||
paddingLeft: '16px',
|
||||
paddingRight: '16px',
|
||||
},
|
||||
}}
|
||||
>
|
||||
<Form form={form} labelCol={{ span: 6 }} wrapperCol={{ span: 16 }} style={{ marginTop: 17 }}>
|
||||
<Form.Item label="MM2 Name">{record.connectorName}</Form.Item>
|
||||
<Form.Item
|
||||
name="connectorName"
|
||||
label="MM2 Name"
|
||||
rules={[
|
||||
// { required: true },
|
||||
() => ({
|
||||
validator(_, value) {
|
||||
if (!value) {
|
||||
return Promise.reject(new Error('请输入MM2 Name名称'));
|
||||
} else if (value !== record.connectorName) {
|
||||
return Promise.reject(new Error('请输入正确的MM2 Name名称'));
|
||||
}
|
||||
return Promise.resolve();
|
||||
},
|
||||
}),
|
||||
]}
|
||||
>
|
||||
<Input placeholder="请输入" size="small"></Input>
|
||||
</Form.Item>
|
||||
</Form>
|
||||
</Modal>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default DeleteConnector;
|
||||
@@ -0,0 +1,185 @@
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import { Drawer, Utils, AppContainer, ProTable, Tabs, Empty, Spin } from 'knowdesign';
|
||||
import API from '@src/api';
|
||||
import MirrorMakerDetailCard from '@src/components/CardBar/MirrorMakerDetailCard';
|
||||
import { defaultPagination, getMM2DetailColumns } from './config';
|
||||
import notification from '@src/components/Notification';
|
||||
import './index.less';
|
||||
const { TabPane } = Tabs;
|
||||
const prefix = 'mm2-detail';
|
||||
const { request } = Utils;
|
||||
|
||||
const DetailTable = ({ loading, retryOption, data }: { loading: boolean; retryOption: any; data: any[] }) => {
|
||||
const [pagination, setPagination] = useState<any>(defaultPagination);
|
||||
const onTableChange = (pagination: any, filters: any, sorter: any) => {
|
||||
setPagination(pagination);
|
||||
};
|
||||
return (
|
||||
<Spin spinning={loading}>
|
||||
{data.length ? (
|
||||
<ProTable
|
||||
key="mm2-detail-table"
|
||||
showQueryForm={false}
|
||||
tableProps={{
|
||||
showHeader: false,
|
||||
rowKey: 'taskId',
|
||||
// loading: loading,
|
||||
columns: getMM2DetailColumns({ retryOption }),
|
||||
dataSource: data,
|
||||
paginationProps: { ...pagination },
|
||||
attrs: {
|
||||
onChange: onTableChange,
|
||||
// scroll: { x: 'max-content' },
|
||||
bordered: false,
|
||||
},
|
||||
}}
|
||||
/>
|
||||
) : (
|
||||
<Empty description="暂无数据" image={Empty.PRESENTED_IMAGE_CUSTOM} style={{ padding: '100px 0' }} />
|
||||
)}
|
||||
</Spin>
|
||||
);
|
||||
};
|
||||
|
||||
const MM2Detail = (props: any) => {
|
||||
const { visible, setVisible, record } = props;
|
||||
const [global] = AppContainer.useGlobalValue();
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [data, setData] = useState([]);
|
||||
|
||||
const [tabSelectType, setTabSelectType] = useState<string>('MirrorSource');
|
||||
const onClose = () => {
|
||||
setVisible(false);
|
||||
setTabSelectType('MirrorSource');
|
||||
// setPagination(defaultPagination);
|
||||
// clean hash
|
||||
};
|
||||
const callback = (key: any) => {
|
||||
setTabSelectType(key);
|
||||
};
|
||||
|
||||
const genData: any = {
|
||||
MirrorSource: async () => {
|
||||
if (global?.clusterInfo?.id === undefined) return;
|
||||
setData([]);
|
||||
setLoading(true);
|
||||
if (record.connectorName) {
|
||||
request(API.getConnectDetailTasks(record.connectorName, record.connectClusterId))
|
||||
.then((res: any) => {
|
||||
setData(res || []);
|
||||
setLoading(false);
|
||||
})
|
||||
.catch((err) => {
|
||||
setLoading(false);
|
||||
});
|
||||
} else {
|
||||
setLoading(false);
|
||||
}
|
||||
},
|
||||
MirrorCheckpoint: async () => {
|
||||
if (global?.clusterInfo?.id === undefined) return;
|
||||
setData([]);
|
||||
setLoading(true);
|
||||
if (record.checkpointConnector) {
|
||||
request(API.getConnectDetailTasks(record.checkpointConnector, record.connectClusterId))
|
||||
.then((res: any) => {
|
||||
setData(res || []);
|
||||
setLoading(false);
|
||||
})
|
||||
.catch((err) => {
|
||||
setLoading(false);
|
||||
});
|
||||
} else {
|
||||
setLoading(false);
|
||||
}
|
||||
},
|
||||
MirrorHeatbeat: async () => {
|
||||
if (global?.clusterInfo?.id === undefined) return;
|
||||
setData([]);
|
||||
setLoading(true);
|
||||
if (record.heartbeatConnector) {
|
||||
request(API.getConnectDetailTasks(record.heartbeatConnector, record.connectClusterId))
|
||||
.then((res: any) => {
|
||||
setData(res || []);
|
||||
setLoading(false);
|
||||
})
|
||||
.catch((err) => {
|
||||
setLoading(false);
|
||||
});
|
||||
} else {
|
||||
setLoading(false);
|
||||
}
|
||||
},
|
||||
};
|
||||
|
||||
const retryOption = (taskId: any) => {
|
||||
const params = {
|
||||
action: 'restart',
|
||||
connectClusterId: record?.connectClusterId,
|
||||
connectorName: record?.connectorName,
|
||||
taskId,
|
||||
};
|
||||
// 需要区分 tabSelectType
|
||||
request(API.optionTasks(), { method: 'PUT', data: params }).then((res: any) => {
|
||||
if (res === null) {
|
||||
notification.success({
|
||||
message: `任务重试成功`,
|
||||
});
|
||||
genData[tabSelectType]();
|
||||
} else {
|
||||
notification.error({
|
||||
message: `任务重试失败`,
|
||||
});
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
visible && record && genData[tabSelectType]();
|
||||
}, [visible, tabSelectType]);
|
||||
|
||||
return (
|
||||
<Drawer
|
||||
// push={false}
|
||||
title={
|
||||
<span>
|
||||
<span style={{ fontSize: '18px', fontFamily: 'PingFangSC-Semibold', color: '#495057' }}>{record.connectorName ?? '-'}</span>
|
||||
</span>
|
||||
}
|
||||
width={1080}
|
||||
placement="right"
|
||||
onClose={onClose}
|
||||
visible={visible}
|
||||
className={`${prefix}-drawer`}
|
||||
destroyOnClose
|
||||
maskClosable={false}
|
||||
>
|
||||
<MirrorMakerDetailCard record={record} tabSelectType={tabSelectType} />
|
||||
<Tabs
|
||||
className={'custom_tabs_class'}
|
||||
defaultActiveKey="Configuration"
|
||||
// activeKey={tabSelectType}
|
||||
onChange={callback}
|
||||
destroyInactiveTabPane
|
||||
>
|
||||
<TabPane tab="MirrorSource" key="MirrorSource">
|
||||
<DetailTable loading={loading} retryOption={retryOption} data={data} />
|
||||
{/* {global.isShowControl && global.isShowControl(ControlStatusMap.BROKER_DETAIL_CONFIG) ? (
|
||||
<Configuration searchKeywords={searchKeywords} tabSelectType={tabSelectType} hashData={hashData} />
|
||||
) : (
|
||||
<Empty description="当前版本过低,不支持该功能!" />
|
||||
)} */}
|
||||
</TabPane>
|
||||
<TabPane tab="MirrorCheckpoint" key="MirrorCheckpoint">
|
||||
<DetailTable loading={loading} retryOption={retryOption} data={data} />
|
||||
</TabPane>
|
||||
<TabPane tab="MirrorHeatbeat" key="MirrorHeatbeat">
|
||||
<DetailTable loading={loading} retryOption={retryOption} data={data} />
|
||||
</TabPane>
|
||||
</Tabs>
|
||||
{/* <BrokerDetailHealthCheck record={{ brokerId: hashData?.brokerId }} /> */}
|
||||
</Drawer>
|
||||
);
|
||||
};
|
||||
|
||||
export default MM2Detail;
|
||||
@@ -0,0 +1,344 @@
|
||||
import SmallChart from '@src/components/SmallChart';
|
||||
import { IconFont } from '@knowdesign/icons';
|
||||
import { Button, Tag, Tooltip, Utils, Popconfirm, AppContainer } from 'knowdesign';
|
||||
import React from 'react';
|
||||
import Delete from './Delete';
|
||||
import { ClustersPermissionMap } from '../CommonConfig';
|
||||
export const defaultPagination = {
|
||||
current: 1,
|
||||
pageSize: 10,
|
||||
position: 'bottomRight',
|
||||
showSizeChanger: true,
|
||||
pageSizeOptions: ['10', '20', '50', '100', '200', '500'],
|
||||
};
|
||||
|
||||
export const optionType: { [name: string]: string } = {
|
||||
['stop']: '暂停',
|
||||
['restart']: '重启',
|
||||
['resume']: '继续',
|
||||
};
|
||||
|
||||
export const stateEnum: any = {
|
||||
['UNASSIGNED']: {
|
||||
// 未分配
|
||||
name: 'Unassigned',
|
||||
color: '#556EE6',
|
||||
bgColor: '#EBEEFA',
|
||||
},
|
||||
['RUNNING']: {
|
||||
// 运行
|
||||
name: 'Running',
|
||||
color: '#00C0A2',
|
||||
bgColor: 'rgba(0,192,162,0.10)',
|
||||
},
|
||||
['PAUSED']: {
|
||||
// 暂停
|
||||
name: 'Paused',
|
||||
color: '#495057',
|
||||
bgColor: '#ECECF6',
|
||||
},
|
||||
['FAILED']: {
|
||||
// 失败
|
||||
name: 'Failed',
|
||||
color: '#F58342',
|
||||
bgColor: '#fef3e5',
|
||||
},
|
||||
['DESTROYED']: {
|
||||
// 销毁
|
||||
name: 'Destroyed',
|
||||
color: '#FF7066',
|
||||
bgColor: '#fdefee',
|
||||
},
|
||||
['RESTARTING']: {
|
||||
// 重新启动
|
||||
name: 'Restarting',
|
||||
color: '#3991FF',
|
||||
bgColor: '#e9f5ff',
|
||||
},
|
||||
};
|
||||
|
||||
const calcCurValue = (record: any, metricName: string) => {
|
||||
// const item = (record.metricPoints || []).find((item: any) => item.metricName === metricName);
|
||||
// return item?.value || '';
|
||||
// TODO 替换record
|
||||
const orgVal = record?.latestMetrics?.metrics?.[metricName];
|
||||
if (orgVal !== undefined) {
|
||||
if (metricName === 'TotalRecordErrors') {
|
||||
return Math.round(orgVal).toLocaleString();
|
||||
} else {
|
||||
return Number(Utils.formatAssignSize(orgVal, 'KB', orgVal > 1000 ? 2 : 3)).toLocaleString();
|
||||
// return Utils.formatAssignSize(orgVal, 'KB');
|
||||
}
|
||||
}
|
||||
return '-';
|
||||
// return orgVal !== undefined ? (metricName !== 'HealthScore' ? formatAssignSize(orgVal, 'KB') : orgVal) : '-';
|
||||
};
|
||||
|
||||
const renderLine = (record: any, metricName: string) => {
|
||||
const points = record.metricLines?.find((item: any) => item.metricName === metricName)?.metricPoints || [];
|
||||
return points.length ? (
|
||||
<div className="metric-data-wrap">
|
||||
<SmallChart
|
||||
width={'100%'}
|
||||
height={30}
|
||||
chartData={{
|
||||
name: record.metricName,
|
||||
data: points.map((item: any) => ({ time: item.timeStamp, value: item.value })),
|
||||
}}
|
||||
/>
|
||||
<span className="cur-val">{calcCurValue(record, metricName)}</span>
|
||||
</div>
|
||||
) : (
|
||||
<span className="cur-val">{calcCurValue(record, metricName)}</span>
|
||||
);
|
||||
};
|
||||
|
||||
export const getMM2Columns = (arg?: any) => {
|
||||
// eslint-disable-next-line react-hooks/rules-of-hooks
|
||||
const [global] = AppContainer.useGlobalValue();
|
||||
const columns: any = [
|
||||
{
|
||||
title: 'MM2 Name',
|
||||
dataIndex: 'connectorName',
|
||||
key: 'connectorName',
|
||||
width: 160,
|
||||
fixed: 'left',
|
||||
lineClampOne: true,
|
||||
render: (t: string, r: any) => {
|
||||
return t ? (
|
||||
<>
|
||||
<Tooltip placement="bottom" title={t}>
|
||||
<a
|
||||
onClick={() => {
|
||||
arg.getDetailInfo(r);
|
||||
}}
|
||||
>
|
||||
{t}
|
||||
</a>
|
||||
</Tooltip>
|
||||
</>
|
||||
) : (
|
||||
'-'
|
||||
);
|
||||
},
|
||||
},
|
||||
{
|
||||
title: 'Connect集群',
|
||||
dataIndex: 'connectClusterName',
|
||||
key: 'connectClusterName',
|
||||
width: 200,
|
||||
lineClampOne: true,
|
||||
needTooltip: true,
|
||||
},
|
||||
{
|
||||
title: 'State',
|
||||
dataIndex: 'state',
|
||||
key: 'state',
|
||||
width: 120,
|
||||
render: (t: string, r: any) => {
|
||||
return t ? (
|
||||
<Tag
|
||||
style={{
|
||||
background: stateEnum[t]?.bgColor,
|
||||
color: stateEnum[t]?.color,
|
||||
padding: '3px 6px',
|
||||
}}
|
||||
>
|
||||
{stateEnum[t]?.name}
|
||||
</Tag>
|
||||
) : (
|
||||
'-'
|
||||
);
|
||||
},
|
||||
},
|
||||
{
|
||||
// title: '集群(源-->目标)',
|
||||
title: (
|
||||
<span>
|
||||
集群(源{' '}
|
||||
<span>
|
||||
<IconFont type="icon-jiantou" />
|
||||
</span>{' '}
|
||||
目标)
|
||||
</span>
|
||||
),
|
||||
dataIndex: 'destKafkaClusterName',
|
||||
key: 'destKafkaClusterName',
|
||||
width: 200,
|
||||
render: (t: string, r: any) => {
|
||||
return r.sourceKafkaClusterName && r.destKafkaClusterName ? (
|
||||
<span>
|
||||
<span>{r.sourceKafkaClusterName} </span>
|
||||
<IconFont type="icon-jiantou" />
|
||||
<span> {t}</span>
|
||||
</span>
|
||||
) : (
|
||||
'-'
|
||||
);
|
||||
},
|
||||
},
|
||||
{
|
||||
title: 'Tasks',
|
||||
dataIndex: 'taskCount',
|
||||
key: 'taskCount',
|
||||
width: 100,
|
||||
},
|
||||
{
|
||||
title: '复制流量速率',
|
||||
dataIndex: 'byteRate',
|
||||
key: 'byteRate',
|
||||
sorter: true,
|
||||
width: 170,
|
||||
render: (value: any, record: any) => renderLine(record, 'ByteRate'),
|
||||
},
|
||||
{
|
||||
title: '消息复制速率',
|
||||
dataIndex: 'recordRate',
|
||||
key: 'recordRate',
|
||||
sorter: true,
|
||||
width: 170,
|
||||
render: (value: any, record: any) => renderLine(record, 'RecordRate'),
|
||||
},
|
||||
{
|
||||
title: '最大延迟',
|
||||
dataIndex: 'replicationLatencyMsMax',
|
||||
key: 'replicationLatencyMsMax',
|
||||
sorter: true,
|
||||
width: 170,
|
||||
render: (value: any, record: any) => renderLine(record, 'ReplicationLatencyMsMax'),
|
||||
},
|
||||
];
|
||||
if (global.hasPermission) {
|
||||
columns.push({
|
||||
title: '操作',
|
||||
dataIndex: 'options',
|
||||
key: 'options',
|
||||
width: 200,
|
||||
filterTitle: true,
|
||||
fixed: 'right',
|
||||
// eslint-disable-next-line react/display-name
|
||||
render: (_t: any, r: any) => {
|
||||
return (
|
||||
<div>
|
||||
{global.hasPermission(ClustersPermissionMap.MM2_STOP_RESUME) && (r.state === 'RUNNING' || r.state === 'PAUSED') && (
|
||||
<Popconfirm
|
||||
title={`是否${r.state === 'RUNNING' ? '暂停' : '继续'}当前任务?`}
|
||||
onConfirm={() => arg?.optionConnect(r, r.state === 'RUNNING' ? 'stop' : 'resume')}
|
||||
// onCancel={cancel}
|
||||
okText="是"
|
||||
cancelText="否"
|
||||
overlayClassName="connect-popconfirm"
|
||||
>
|
||||
<Button key="stopResume" type="link" size="small">
|
||||
{/* {r?.state !== 1 ? '继续' : '暂停'} */}
|
||||
{r.state === 'RUNNING' ? '暂停' : '继续'}
|
||||
</Button>
|
||||
</Popconfirm>
|
||||
)}
|
||||
{global.hasPermission(ClustersPermissionMap.MM2_RESTART) ? (
|
||||
<Popconfirm
|
||||
title="是否重启当前任务?"
|
||||
onConfirm={() => arg?.optionConnect(r, 'restart')}
|
||||
// onCancel={cancel}
|
||||
okText="是"
|
||||
cancelText="否"
|
||||
overlayClassName="connect-popconfirm"
|
||||
>
|
||||
<Button key="restart" type="link" size="small">
|
||||
重启
|
||||
</Button>
|
||||
</Popconfirm>
|
||||
) : (
|
||||
<></>
|
||||
)}
|
||||
{global.hasPermission(ClustersPermissionMap.MM2_CHANGE_CONFIG) ? (
|
||||
r.sourceKafkaClusterId ? (
|
||||
<Button type="link" size="small" onClick={() => arg?.editConnector(r)}>
|
||||
编辑
|
||||
</Button>
|
||||
) : (
|
||||
<Tooltip title="非本平台创建的任务无法编辑">
|
||||
<Button type="link" disabled size="small">
|
||||
编辑
|
||||
</Button>
|
||||
</Tooltip>
|
||||
)
|
||||
) : (
|
||||
<></>
|
||||
)}
|
||||
{global.hasPermission(ClustersPermissionMap.MM2_DELETE) ? <Delete record={r} onConfirm={arg?.deleteTesk}></Delete> : <></>}
|
||||
</div>
|
||||
);
|
||||
},
|
||||
});
|
||||
}
|
||||
return columns;
|
||||
};
|
||||
|
||||
// Detail
|
||||
export const getMM2DetailColumns = (arg?: any) => {
|
||||
const columns = [
|
||||
{
|
||||
title: 'Task ID',
|
||||
dataIndex: 'taskId',
|
||||
key: 'taskId',
|
||||
width: 240,
|
||||
render: (t: any, r: any) => {
|
||||
return (
|
||||
<span>
|
||||
{t}
|
||||
{
|
||||
<Tag
|
||||
style={{
|
||||
background: stateEnum[r?.state]?.bgColor,
|
||||
color: stateEnum[r?.state]?.color,
|
||||
padding: '3px 6px',
|
||||
marginLeft: '5px',
|
||||
}}
|
||||
>
|
||||
{Utils.firstCharUppercase(r?.state as string)}
|
||||
</Tag>
|
||||
}
|
||||
</span>
|
||||
);
|
||||
},
|
||||
},
|
||||
{
|
||||
title: 'Worker',
|
||||
dataIndex: 'workerId',
|
||||
key: 'workerId',
|
||||
width: 240,
|
||||
},
|
||||
{
|
||||
title: '错误原因',
|
||||
dataIndex: 'trace',
|
||||
key: 'trace',
|
||||
width: 400,
|
||||
needTooltip: true,
|
||||
lineClampOne: true,
|
||||
},
|
||||
{
|
||||
title: '操作',
|
||||
dataIndex: 'role',
|
||||
key: 'role',
|
||||
width: 100,
|
||||
render: (_t: any, r: any) => {
|
||||
return (
|
||||
<div>
|
||||
<Popconfirm
|
||||
title="是否重试当前任务?"
|
||||
onConfirm={() => arg?.retryOption(r.taskId)}
|
||||
// onCancel={cancel}
|
||||
okText="是"
|
||||
cancelText="否"
|
||||
overlayClassName="connect-popconfirm"
|
||||
>
|
||||
<a>重试</a>
|
||||
</Popconfirm>
|
||||
</div>
|
||||
);
|
||||
},
|
||||
},
|
||||
];
|
||||
return columns;
|
||||
};
|
||||
@@ -0,0 +1,265 @@
|
||||
// mm2列表 图表
|
||||
.metric-data-wrap {
|
||||
// display: flex;
|
||||
// align-items: center;
|
||||
width: 100%;
|
||||
.cur-val {
|
||||
display: block;
|
||||
text-align: right;
|
||||
}
|
||||
.dcloud-spin-nested-loading {
|
||||
flex: 1;
|
||||
}
|
||||
}
|
||||
|
||||
// 新增按钮
|
||||
.add-connect {
|
||||
.dcloud-btn-primary:hover,
|
||||
.dcloud-btn-primary:focus {
|
||||
// 可以控制新增按钮的hover和focus的样式
|
||||
// background: #556ee6;
|
||||
// border-color: #556ee6;
|
||||
}
|
||||
&-btn {
|
||||
border-top-right-radius: 0 !important;
|
||||
border-bottom-right-radius: 0 !important;
|
||||
border-right: none;
|
||||
}
|
||||
&-dropdown-menu {
|
||||
.dcloud-dropdown-menu {
|
||||
border-radius: 8px;
|
||||
&-item {
|
||||
font-size: 13px;
|
||||
}
|
||||
}
|
||||
}
|
||||
&-json {
|
||||
padding: 8px 12px !important;
|
||||
border-top-left-radius: 0 !important;
|
||||
border-bottom-left-radius: 0 !important;
|
||||
border-left: none;
|
||||
}
|
||||
}
|
||||
|
||||
// connect详情
|
||||
.mm2-detail-drawer {
|
||||
.card-bar-container {
|
||||
background: rgba(86, 110, 230, 0.04) !important;
|
||||
|
||||
.card-bar-colunms {
|
||||
background-color: rgba(86, 110, 230, 0);
|
||||
}
|
||||
}
|
||||
&-title {
|
||||
margin: 20px 0 8px;
|
||||
font-family: @font-family-bold;
|
||||
font-size: 16px;
|
||||
font-weight: 500;
|
||||
}
|
||||
}
|
||||
|
||||
// 删除
|
||||
.del-connect-modal {
|
||||
.tip-info {
|
||||
display: flex;
|
||||
color: #592d00;
|
||||
padding: 6px 14px;
|
||||
font-size: 13px;
|
||||
background: #fffae0;
|
||||
border-radius: 4px;
|
||||
.anticon {
|
||||
color: #ffc300;
|
||||
margin-right: 4px;
|
||||
margin-top: 3px;
|
||||
}
|
||||
.test-right-away {
|
||||
color: #556ee6;
|
||||
cursor: pointer;
|
||||
}
|
||||
.dcloud-alert-content {
|
||||
flex: none;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 重启、继续/暂停 气泡卡片
|
||||
.connect-popconfirm {
|
||||
.dcloud-popover-inner-content {
|
||||
padding: 6px 16px;
|
||||
}
|
||||
.dcloud-popover-inner {
|
||||
border-radius: 8px;
|
||||
}
|
||||
.dcloud-popover-message,
|
||||
.dcloud-btn {
|
||||
font-size: 12px !important;
|
||||
}
|
||||
}
|
||||
|
||||
.operate-connector-drawer {
|
||||
.connector-plugin-desc {
|
||||
font-size: 13px;
|
||||
.connector-plugin-title {
|
||||
font-family: @font-family-bold;
|
||||
}
|
||||
}
|
||||
.dcloud-collapse.add-connector-collapse {
|
||||
.add-connector-collapse-panel,
|
||||
.add-connector-collapse-panel:last-child {
|
||||
margin-bottom: 8px;
|
||||
overflow: hidden;
|
||||
background: #f8f9fa;
|
||||
border: 0px;
|
||||
border-radius: 8px;
|
||||
.dcloud-collapse-header {
|
||||
padding: 8px 12px;
|
||||
font-size: 14px;
|
||||
color: #495057;
|
||||
.dcloud-collapse-arrow {
|
||||
margin-right: 8px !important;
|
||||
font-size: 10px;
|
||||
}
|
||||
}
|
||||
&:hover .dcloud-collapse-extra {
|
||||
opacity: 1;
|
||||
}
|
||||
&:not(.dcloud-collapse-item-active) {
|
||||
.dcloud-collapse-header:hover {
|
||||
background: #f1f3ff;
|
||||
}
|
||||
}
|
||||
.dcloud-collapse-content-box {
|
||||
display: flex;
|
||||
flex-flow: row wrap;
|
||||
justify-content: space-between;
|
||||
padding: 20px 14px 0 14px;
|
||||
.dcloud-form-item {
|
||||
flex: 1 0 50%;
|
||||
.dcloud-input-number {
|
||||
width: 100%;
|
||||
}
|
||||
&:nth-child(2n) {
|
||||
padding-left: 6px;
|
||||
}
|
||||
&:nth-child(2n + 1) {
|
||||
padding-right: 6px;
|
||||
}
|
||||
.dcloud-form-item-control-input {
|
||||
height: 100%;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.add-container-plugin-select {
|
||||
height: 27px;
|
||||
margin: 4px 0;
|
||||
position: relative;
|
||||
.dcloud-form-item {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
.dcloud-form-item-control-input {
|
||||
min-height: 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.dcloud-alert {
|
||||
margin: 16px 0 24px 0;
|
||||
padding: 0 12px;
|
||||
border: unset;
|
||||
border-radius: 8px;
|
||||
background: #fffae0;
|
||||
.dcloud-alert-message {
|
||||
font-size: 13px;
|
||||
color: #592d00;
|
||||
.dcloud-btn {
|
||||
padding: 0 4px;
|
||||
font-size: 13px;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.operate-connector-drawer-use-json {
|
||||
.CodeMirror.cm-s-default {
|
||||
height: calc(100vh - 146px);
|
||||
}
|
||||
.dcloud-form-item {
|
||||
margin-bottom: 0 !important;
|
||||
}
|
||||
}
|
||||
|
||||
.mirror-maker-steps {
|
||||
width: 340px !important;
|
||||
margin: 0 auto !important;
|
||||
}
|
||||
|
||||
.add-mm2-config-title {
|
||||
color: #556ee6;
|
||||
margin-bottom: 24px;
|
||||
display: inline-block;
|
||||
cursor: pointer;
|
||||
&-text {
|
||||
margin-right: 7px;
|
||||
}
|
||||
&-icon {
|
||||
transform: rotate(180deg);
|
||||
}
|
||||
}
|
||||
|
||||
.custom-form-item-27 {
|
||||
padding: 0 16px;
|
||||
.dcloud-form-item {
|
||||
margin-bottom: 6px !important;
|
||||
}
|
||||
.dcloud-form-item-label > label {
|
||||
height: 27px;
|
||||
}
|
||||
.group-offset-table {
|
||||
margin-bottom: 40px;
|
||||
margin-top: 12px;
|
||||
}
|
||||
}
|
||||
|
||||
.custom-form-item-36 {
|
||||
.dcloud-form-item-control-input {
|
||||
min-height: 36px;
|
||||
}
|
||||
}
|
||||
|
||||
.clear-topic-minHeight {
|
||||
.dcloud-form-item-control-input {
|
||||
min-height: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.add-mm2-flex-layout {
|
||||
&-item {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
}
|
||||
.senior-config-left {
|
||||
width: 228px !important;
|
||||
margin-right: 10px;
|
||||
// .dcloud-form-item-label {
|
||||
// width: 190px !important;
|
||||
// text-align: right !important;
|
||||
// }
|
||||
}
|
||||
.dcloud-form-item {
|
||||
width: 345px;
|
||||
flex-direction: row !important;
|
||||
height: 27px;
|
||||
margin-bottom: 2px !important;
|
||||
&-label {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: flex-end;
|
||||
padding: 0 !important;
|
||||
margin-right: 10px;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,240 @@
|
||||
import React, { useState, useEffect, useRef } from 'react';
|
||||
import { ProTable, Dropdown, Button, Utils, AppContainer, SearchInput, Menu } from 'knowdesign';
|
||||
import { IconFont } from '@knowdesign/icons';
|
||||
import API from '../../api';
|
||||
import { getMM2Columns, defaultPagination, optionType } from './config';
|
||||
import { tableHeaderPrefix } from '@src/constants/common';
|
||||
import MirrorMakerCard from '@src/components/CardBar/MirrorMakerCard';
|
||||
import DBreadcrumb from 'knowdesign/es/extend/d-breadcrumb';
|
||||
import AddMM2, { OperateInfo } from './AddMM2';
|
||||
import MM2Detail from './Detail';
|
||||
import notification from '@src/components/Notification';
|
||||
import './index.less';
|
||||
import AddConnectorUseJSON from './AddMM2JSON';
|
||||
import { ClustersPermissionMap } from '../CommonConfig';
|
||||
const { request } = Utils;
|
||||
|
||||
const rateMap: any = {
|
||||
byteRate: ['ByteRate'],
|
||||
recordRate: ['RecordRate'],
|
||||
replicationLatencyMsMax: ['ReplicationLatencyMsMax'],
|
||||
};
|
||||
|
||||
const MirrorMaker2: React.FC = () => {
|
||||
const [global] = AppContainer.useGlobalValue();
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [detailVisible, setDetailVisible] = useState(false);
|
||||
const [data, setData] = useState([]);
|
||||
const [searchKeywords, setSearchKeywords] = useState('');
|
||||
const [pagination, setPagination] = useState<any>(defaultPagination);
|
||||
const [sortInfo, setSortInfo] = useState({});
|
||||
const [detailRecord, setDetailRecord] = useState('');
|
||||
const [healthType, setHealthType] = useState(true);
|
||||
const addConnectorRef = useRef(null);
|
||||
const addConnectorJsonRef = useRef(null);
|
||||
|
||||
const getRecent1DayTimeStamp = () => [Date.now() - 24 * 60 * 60 * 1000, Date.now()];
|
||||
// 请求接口获取数据
|
||||
const genData = async ({ pageNo, pageSize, filters, sorter }: any) => {
|
||||
const [startStamp, endStamp] = getRecent1DayTimeStamp();
|
||||
if (global?.clusterInfo?.id === undefined) return;
|
||||
setLoading(true);
|
||||
const params = {
|
||||
metricLines: {
|
||||
aggType: 'avg',
|
||||
endTime: endStamp,
|
||||
metricsNames: ['ByteRate', 'RecordRate', 'ReplicationLatencyMsMax'],
|
||||
// metricsNames: ['SourceRecordPollRate', 'SourceRecordWriteRate', 'SinkRecordReadRate', 'SinkRecordSendRate', 'TotalRecordErrors'],
|
||||
startTime: startStamp,
|
||||
topNu: 0,
|
||||
},
|
||||
searchKeywords: searchKeywords.slice(0, 128),
|
||||
pageNo,
|
||||
pageSize,
|
||||
latestMetricNames: ['ByteRate', 'RecordRate', 'ReplicationLatencyMsMax'],
|
||||
// latestMetricNames: ['SourceRecordPollRate', 'SourceRecordWriteRate', 'SinkRecordReadRate', 'SinkRecordSendRate', 'TotalRecordErrors'],
|
||||
sortType: sorter?.order ? sorter.order.substring(0, sorter.order.indexOf('end')) : 'desc',
|
||||
sortMetricNameList: rateMap[sorter?.field] || [],
|
||||
};
|
||||
|
||||
request(API.getMirrorMakerList(global?.clusterInfo?.id), { method: 'POST', data: params })
|
||||
// request(API.getConnectorsList(global?.clusterInfo?.id), { method: 'POST', data: params })
|
||||
.then((res: any) => {
|
||||
setPagination({
|
||||
current: res.pagination?.pageNo,
|
||||
pageSize: res.pagination?.pageSize,
|
||||
total: res.pagination?.total,
|
||||
});
|
||||
const newData =
|
||||
res?.bizData.map((item: any) => {
|
||||
return {
|
||||
...item,
|
||||
...item?.latestMetrics?.metrics,
|
||||
key: item.connectClusterName + item.connectorName,
|
||||
};
|
||||
}) || [];
|
||||
setData(newData);
|
||||
setLoading(false);
|
||||
})
|
||||
.catch((err) => {
|
||||
setLoading(false);
|
||||
});
|
||||
};
|
||||
|
||||
const onTableChange = (pagination: any, filters: any, sorter: any) => {
|
||||
setSortInfo(sorter);
|
||||
genData({ pageNo: pagination.current, pageSize: pagination.pageSize, filters, sorter });
|
||||
};
|
||||
|
||||
const menu = (
|
||||
<Menu className="">
|
||||
<Menu.Item>
|
||||
<span onClick={() => addConnectorJsonRef.current?.onOpen('create')}>JSON 新增MM2</span>
|
||||
</Menu.Item>
|
||||
</Menu>
|
||||
);
|
||||
|
||||
const getDetailInfo = (record: any) => {
|
||||
setDetailRecord(record);
|
||||
setDetailVisible(true);
|
||||
};
|
||||
|
||||
// 编辑
|
||||
const editConnector = (detail: OperateInfo['detail']) => {
|
||||
addConnectorRef.current?.onOpen('edit', addConnectorJsonRef.current, detail);
|
||||
};
|
||||
|
||||
// 重启、暂停/继续 操作
|
||||
const optionConnect = (record: any, action: string) => {
|
||||
setLoading(true);
|
||||
const params = {
|
||||
action,
|
||||
connectClusterId: record?.connectClusterId,
|
||||
connectorName: record?.connectorName,
|
||||
};
|
||||
|
||||
request(API.mirrorMakerOperates, { method: 'PUT', data: params })
|
||||
.then((res: any) => {
|
||||
if (res === null) {
|
||||
notification.success({
|
||||
message: `任务已${optionType[params.action]}`,
|
||||
description: `任务状态更新会有至多1min延迟`,
|
||||
});
|
||||
genData({ pageNo: pagination.current, pageSize: pagination.pageSize, sorter: sortInfo });
|
||||
setHealthType(!healthType);
|
||||
} else {
|
||||
notification.error({
|
||||
message: `${optionType[params.action]}任务失败`,
|
||||
});
|
||||
}
|
||||
})
|
||||
.catch((err) => {
|
||||
setLoading(false);
|
||||
});
|
||||
};
|
||||
|
||||
// 删除任务
|
||||
const deleteTesk = () => {
|
||||
genData({ pageNo: 1, pageSize: pagination.pageSize });
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
genData({
|
||||
pageNo: 1,
|
||||
pageSize: pagination.pageSize,
|
||||
sorter: sortInfo,
|
||||
});
|
||||
}, [searchKeywords]);
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className="breadcrumb" style={{ marginBottom: '10px' }}>
|
||||
<DBreadcrumb
|
||||
breadcrumbs={[
|
||||
{ label: '多集群管理', aHref: '/' },
|
||||
{ label: global?.clusterInfo?.name, aHref: `/cluster/${global?.clusterInfo?.id}` },
|
||||
{ label: 'Replication', aHref: `/cluster/${global?.clusterInfo?.id}/replication` },
|
||||
{ label: 'Mirror Maker', aHref: `` },
|
||||
]}
|
||||
/>
|
||||
</div>
|
||||
{/* <HasConnector>
|
||||
<>
|
||||
|
||||
</>
|
||||
</HasConnector> */}
|
||||
<div style={{ margin: '12px 0' }}>
|
||||
<MirrorMakerCard state={healthType} />
|
||||
</div>
|
||||
<div className="custom-table-content">
|
||||
<div className={tableHeaderPrefix}>
|
||||
<div className={`${tableHeaderPrefix}-left`}>
|
||||
<div
|
||||
className={`${tableHeaderPrefix}-left-refresh`}
|
||||
onClick={() => genData({ pageNo: pagination.current, pageSize: pagination.pageSize })}
|
||||
>
|
||||
<IconFont className={`${tableHeaderPrefix}-left-refresh-icon`} type="icon-shuaxin1" />
|
||||
</div>
|
||||
</div>
|
||||
<div className={`${tableHeaderPrefix}-right`}>
|
||||
<SearchInput
|
||||
onSearch={setSearchKeywords}
|
||||
attrs={{
|
||||
placeholder: '请输入MM2 Name',
|
||||
style: { width: '248px', borderRiadus: '8px' },
|
||||
maxLength: 128,
|
||||
}}
|
||||
/>
|
||||
{global.hasPermission && global.hasPermission(ClustersPermissionMap.MM2_ADD) ? (
|
||||
<span className="add-connect">
|
||||
<Button
|
||||
className="add-connect-btn"
|
||||
icon={<IconFont type="icon-jiahao" />}
|
||||
type="primary"
|
||||
onClick={() => addConnectorRef.current?.onOpen('create', addConnectorJsonRef.current)}
|
||||
>
|
||||
新增MM2
|
||||
</Button>
|
||||
<Dropdown overlayClassName="add-connect-dropdown-menu" overlay={menu}>
|
||||
<Button className="add-connect-json" type="primary">
|
||||
<IconFont type="icon-guanwangxiala" />
|
||||
</Button>
|
||||
</Dropdown>
|
||||
</span>
|
||||
) : (
|
||||
<></>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
<ProTable
|
||||
key="mirror-maker-table"
|
||||
showQueryForm={false}
|
||||
tableProps={{
|
||||
showHeader: false,
|
||||
rowKey: 'key',
|
||||
loading: loading,
|
||||
columns: getMM2Columns({ getDetailInfo, deleteTesk, optionConnect, editConnector }),
|
||||
dataSource: data,
|
||||
paginationProps: { ...pagination },
|
||||
attrs: {
|
||||
onChange: onTableChange,
|
||||
scroll: { x: 'max-content', y: 'calc(100vh - 400px)' },
|
||||
bordered: false,
|
||||
},
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
<MM2Detail visible={detailVisible} setVisible={setDetailVisible} record={detailRecord} />
|
||||
<AddMM2
|
||||
ref={addConnectorRef}
|
||||
refresh={() => genData({ pageNo: pagination.current, pageSize: pagination.pageSize, sorter: sortInfo })}
|
||||
/>
|
||||
<AddConnectorUseJSON
|
||||
ref={addConnectorJsonRef}
|
||||
refresh={() => genData({ pageNo: pagination.current, pageSize: pagination.pageSize, sorter: sortInfo })}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default MirrorMaker2;
|
||||
@@ -29,6 +29,9 @@ import ConnectDashboard from './ConnectDashboard';
|
||||
import Connectors from './Connect';
|
||||
import Workers from './Connect/Workers';
|
||||
|
||||
import MirrorMaker2 from './MirrorMaker2';
|
||||
import MirrorMakerDashboard from './MirrorMakerDashBoard';
|
||||
|
||||
const pageRoutes = [
|
||||
{
|
||||
path: '/',
|
||||
@@ -152,6 +155,18 @@ const pageRoutes = [
|
||||
component: Workers,
|
||||
noSider: false,
|
||||
},
|
||||
{
|
||||
path: 'replication',
|
||||
exact: true,
|
||||
component: MirrorMakerDashboard,
|
||||
noSider: false,
|
||||
},
|
||||
{
|
||||
path: 'replication/mirror-maker',
|
||||
exact: true,
|
||||
component: MirrorMaker2,
|
||||
noSider: false,
|
||||
},
|
||||
{
|
||||
path: 'security/acls',
|
||||
exact: true,
|
||||
|
||||
Reference in New Issue
Block a user