v2.8.1_e初始化

1、测试代码,开源用户尽量不要使用;
2、包含Kafka-HA的相关功能,在v2.8.0_e的基础上,补充按照clientId切换的功能;
3、基于v2.8.0_e拉的分支;
This commit is contained in:
zengqiao
2023-02-13 16:48:59 +08:00
parent e81c0f3040
commit b16a7b9bff
44 changed files with 1759 additions and 611 deletions

View File

@@ -4,10 +4,10 @@
"description": "",
"scripts": {
"prestart": "npm install --save-dev webpack-dev-server",
"start": "webpack serve",
"start": "webpack-dev-server",
"daily-build": "cross-env NODE_ENV=production webpack",
"pre-build": "cross-env NODE_ENV=production webpack",
"prod-build": "cross-env NODE_ENV=production webpack",
"prod-build": "cross-env NODE_OPTIONS=--max-old-space-size=8000 NODE_ENV=production webpack",
"fix-memory": "cross-env LIMIT=4096 increase-memory-limit"
},
"author": "",
@@ -52,10 +52,11 @@
"typescript": "^3.3.3333",
"url-loader": "^4.1.1",
"webpack": "^4.29.6",
"webpack-cli": "^4.9.1",
"webpack-cli": "^3.2.3",
"webpack-dev-server": "^3.11.3",
"xlsx": "^0.16.1"
},
"dependencies": {
"format-to-json": "^1.0.4"
}
}
}

View File

@@ -2,9 +2,11 @@ import * as React from 'react';
import { admin } from 'store/admin';
import { Modal, Form, Radio } from 'antd';
import { IBrokersMetadata, IBrokersRegions, IMetaData } from 'types/base-type';
import { Alert, message, notification, Table, Tooltip, Transfer } from 'component/antd';
import { getClusterHaTopicsStatus, setHaTopics, unbindHaTopics } from 'lib/api';
import { Alert, message, notification, Table, Tooltip, Spin } from 'component/antd';
import { getClusterHaTopicsStatus, getAppRelatedTopics, setHaTopics, unbindHaTopics } from 'lib/api';
import { cellStyle } from 'constants/table';
import { renderAttributes, TransferTable, IKafkaUser } from './TopicHaSwitch'
import './index.less'
const layout = {
labelCol: { span: 3 },
@@ -24,6 +26,7 @@ interface IHaTopic {
clusterId: number;
clusterName: string;
haRelation: number;
isHaRelation: boolean;
topicName: string;
key: string;
disabled?: boolean;
@@ -68,27 +71,104 @@ const resColumns = [
},
},
];
const columns = [
{
dataIndex: 'topicName',
title: '名称',
width: 260,
ellipsis: true,
},
];
const kafkaUserColumn = [
{
dataIndex: 'kafkaUser',
title: 'kafkaUser',
width: 100,
ellipsis: true,
},
{
dataIndex: 'manualSelectedTopics',
title: '已选中Topic',
width: 120,
render: (text: string[]) => {
return text?.length ? renderAttributes({ data: text, limit: 3 }) : '-';
},
},
{
dataIndex: 'autoSelectedTopics',
title: '选中关联Topic',
width: 120,
render: (text: string[]) => {
return text?.length ? renderAttributes({ data: text, limit: 3 }) : '-';
},
},
];
class TopicHaRelation extends React.Component<IXFormProps> {
public state = {
radioCheck: 'spec',
haTopics: [] as IHaTopic[],
topics: [] as IHaTopic[],
kafkaUsers: [] as IKafkaUser[],
targetKeys: [] as string[],
selectedKeys: [] as string[],
confirmLoading: false,
firstMove: true,
primaryActiveKeys: [] as string[],
primaryStandbyKeys: [] as string[],
manualSelectedKeys: [] as string[],
spinLoading: false,
};
public selectSingle = null as boolean;
public manualSelectedNames = [] as string[];
public setSelectSingle = (val: boolean) => {
this.selectSingle = val;
}
public setManualSelectedNames = (keys: string[]) => {
// this.manualSelectedNames = this.getTopicsByKeys(keys);
this.manualSelectedNames = keys;
}
public filterManualSelectedKeys = (key: string, selected: boolean) => {
const newManualSelectedKeys = [...this.state.manualSelectedKeys];
const index = newManualSelectedKeys.findIndex(item => item === key);
if (selected) {
if (index === -1) newManualSelectedKeys.push(key);
} else {
if (index !== -1) newManualSelectedKeys.splice(index, 1);
}
this.setManualSelectedNames(newManualSelectedKeys);
this.setState({
manualSelectedKeys: newManualSelectedKeys,
});
}
public getManualSelected = (single: boolean, key?: any, selected?: boolean) => {
this.setSelectSingle(single);
if (single) {
this.filterManualSelectedKeys(key, selected);
} else {
this.setManualSelectedNames(key);
this.setState({
manualSelectedKeys: key,
});
}
}
public handleOk = () => {
this.props.form.validateFields((err: any, values: any) => {
const unbindTopics = [];
const bindTopics = [];
const { primaryStandbyKeys, targetKeys} = this.state;
const unbindKeys = [];
const bindKeys = [];
if (values.rule === 'all') {
setHaTopics({
all: true,
activeClusterId: this.props.currentCluster.clusterId,
standbyClusterId: this.props.currentCluster.haClusterVO.clusterId,
standbyClusterId: this.props.currentCluster.haClusterVO?.clusterId,
topicNames: [],
}).then(res => {
handleMsg(res, '关联成功');
@@ -100,18 +180,18 @@ class TopicHaRelation extends React.Component<IXFormProps> {
return;
}
for (const item of this.state.primaryStandbyKeys) {
if (!this.state.targetKeys.includes(item)) {
unbindTopics.push(item);
for (const item of primaryStandbyKeys) {
if (!targetKeys.includes(item)) {
unbindKeys.push(item);
}
}
for (const item of this.state.targetKeys) {
if (!this.state.primaryStandbyKeys.includes(item)) {
bindTopics.push(item);
for (const item of targetKeys) {
if (!primaryStandbyKeys.includes(item)) {
bindKeys.push(item);
}
}
if (!unbindTopics.length && !bindTopics.length) {
if (!unbindKeys.length && !bindKeys.length) {
return message.info('请选择您要操作的Topic');
}
@@ -140,15 +220,16 @@ class TopicHaRelation extends React.Component<IXFormProps> {
this.props.reload();
};
if (bindTopics.length) {
if (bindKeys.length) {
this.setState({
confirmLoading: true,
});
setHaTopics({
all: false,
activeClusterId: this.props.currentCluster.clusterId,
standbyClusterId: this.props.currentCluster.haClusterVO.clusterId,
topicNames: bindTopics,
standbyClusterId: this.props.currentCluster.haClusterVO?.clusterId,
// topicNames: this.getTopicsByKeys(bindKeys),
topicNames: bindKeys,
}).then(res => {
this.setState({
confirmLoading: false,
@@ -158,15 +239,17 @@ class TopicHaRelation extends React.Component<IXFormProps> {
});
}
if (unbindTopics.length) {
if (unbindKeys.length) {
this.setState({
confirmLoading: true,
});
unbindHaTopics({
all: false,
activeClusterId: this.props.currentCluster.clusterId,
standbyClusterId: this.props.currentCluster.haClusterVO.clusterId,
topicNames: unbindTopics,
standbyClusterId: this.props.currentCluster.haClusterVO?.clusterId,
// topicNames: this.getTopicsByKeys(unbindKeys),
topicNames: unbindKeys,
retainStandbyResource: values.retainStandbyResource,
}).then(res => {
this.setState({
confirmLoading: false,
@@ -194,43 +277,253 @@ class TopicHaRelation extends React.Component<IXFormProps> {
let isReset = false;
// 判断当前移动是否还原为最初的状态
if (primaryStandbyKeys.length === targetKeys.length) {
targetKeys.sort((a, b) => +a - (+b));
primaryStandbyKeys.sort((a, b) => +a - (+b));
let i = 0;
while (i < targetKeys.length) {
if (targetKeys[i] === primaryStandbyKeys[i]) {
i++;
} else {
break;
}
}
isReset = i === targetKeys.length;
const diff = targetKeys.find(item => primaryStandbyKeys.indexOf(item) < 0);
isReset = diff ? false : true;
}
return isReset;
}
public getNewSelectKeys = (removeKeys: string[], selectedKeys: string[]) => {
const { topics, kafkaUsers } = this.state;
// 根据移除的key找与该key关联的其他key一起移除
let relatedTopics: string[] = [];
const relatedKeys: string[] = [];
const newSelectKeys = [];
for (const key of removeKeys) {
const topicName = topics.find(row => row.key === key)?.topicName;
for (const item of kafkaUsers) {
if (item.selectedTopicNameList.includes(topicName)) {
relatedTopics = relatedTopics.concat(item.selectedTopicNameList, item.notSelectTopicNameList);
}
}
for (const item of relatedTopics) {
const key = topics.find(row => row.topicName === item)?.key;
if (key) {
relatedKeys.push(key);
}
}
for (const key of selectedKeys) {
if (!relatedKeys.includes(key)) {
newSelectKeys.push(key);
}
}
}
return newSelectKeys;
}
public setTopicsStatus = (targetKeys: string[], disabled: boolean, isAll = false) => {
const { haTopics } = this.state;
const newTopics = Array.from(haTopics);
const { topics } = this.state;
const newTopics = Array.from(topics);
if (isAll) {
for (let i = 0; i < haTopics.length; i++) {
for (let i = 0; i < topics.length; i++) {
newTopics[i].disabled = disabled;
}
} else {
for (const key of targetKeys) {
const index = haTopics.findIndex(item => item.key === key);
const index = topics.findIndex(item => item.key === key);
if (index > -1) {
newTopics[index].disabled = disabled;
}
}
}
this.setState(({
haTopics: newTopics,
topics: newTopics,
}));
}
public onTransferChange = (targetKeys: string[], direction: string, moveKeys: string[]) => {
const { primaryStandbyKeys, firstMove, primaryActiveKeys } = this.state;
public getTopicsByKeys = (keys: string[]) => {
// 依据key值找topicName
const topicNames: string[] = [];
for (const key of keys) {
const topicName = this.state.topics.find(item => item.key === key)?.topicName;
if (topicName) {
topicNames.push(topicName);
}
}
return topicNames;
}
public getNewKafkaUser = (targetKeys: string[]) => {
const { primaryStandbyKeys, kafkaUsers, topics } = this.state;
const removeKeys = [];
const addKeys = [];
for (const key of primaryStandbyKeys) {
if (targetKeys.indexOf(key) < 0) {
// 移除的
removeKeys.push(key);
}
}
for (const key of targetKeys) {
if (primaryStandbyKeys.indexOf(key) < 0) {
// 新增的
addKeys.push(key);
}
}
const keepKeys = [...removeKeys, ...addKeys];
const newKafkaUsers = kafkaUsers;
const moveTopics = this.getTopicsByKeys(keepKeys);
for (const topic of moveTopics) {
for (const item of newKafkaUsers) {
if (item.selectedTopicNameList.includes(topic)) {
item.show = true;
}
}
}
const showKafaUsers = newKafkaUsers.filter(item => item.show === true);
for (const item of showKafaUsers) {
let i = 0;
while (i < moveTopics.length) {
if (!item.selectedTopicNameList.includes(moveTopics[i])) {
i++;
} else {
break;
}
}
// 表示该kafkaUser不该展示
if (i === moveTopics.length) {
item.show = false;
}
}
return showKafaUsers;
}
public getAppRelatedTopicList = (selectedKeys: string[]) => {
const { topics, targetKeys, primaryStandbyKeys, kafkaUsers } = this.state;
const filterTopicNameList = this.getTopicsByKeys(selectedKeys);
const isReset = this.isPrimaryStatus(targetKeys);
if (!filterTopicNameList.length && isReset) {
// targetKeys
this.setState({
kafkaUsers: kafkaUsers.map(item => ({
...item,
show: false,
})),
});
return;
} else {
// 保留选中项与移动的的项
this.setState({
kafkaUsers: this.getNewKafkaUser(targetKeys),
});
}
// 单向选择所以取当前值的clusterId
const clusterInfo = topics.find(item => item.topicName === filterTopicNameList[0]);
const clusterPhyId = clusterInfo?.clusterId;
if (!clusterPhyId) return;
this.setState({spinLoading: true});
getAppRelatedTopics({
clusterPhyId,
filterTopicNameList,
ha: clusterInfo.isHaRelation,
useKafkaUserAndClientId: false,
}).then((res: IKafkaUser[]) => {
let notSelectTopicNames: string[] = [];
const notSelectTopicKeys: string[] = [];
for (const item of (res || [])) {
notSelectTopicNames = notSelectTopicNames.concat(item.notSelectTopicNameList || []);
}
for (const item of notSelectTopicNames) {
const key = topics.find(row => row.topicName === item)?.key;
if (key && notSelectTopicKeys.indexOf(key) < 0) {
notSelectTopicKeys.push(key);
}
}
const newSelectedKeys = selectedKeys.concat(notSelectTopicKeys);
const newKafkaUsers = (res || []).map(item => ({
...item,
show: true,
manualSelectedTopics: item.selectedTopicNameList.filter(topic => this.manualSelectedNames.indexOf(topic) > -1),
autoSelectedTopics: [...item.selectedTopicNameList, ...item.notSelectTopicNameList].filter(topic => this.manualSelectedNames.indexOf(topic) === -1),
}));
const { kafkaUsers } = this.state;
for (const item of kafkaUsers) {
const resItem = res.find(row => row.kafkaUser === item.kafkaUser);
if (!resItem) {
newKafkaUsers.push(item);
}
}
this.setState({
kafkaUsers: newKafkaUsers,
selectedKeys: newSelectedKeys,
});
if (notSelectTopicKeys.length) {
this.getAppRelatedTopicList(newSelectedKeys);
}
}).finally(() => {
this.setState({spinLoading: false});
});
}
public getRelatedKeys = (currentKeys: string[]) => {
// 未被选中的项
const removeKeys = [];
// 对比上一次记录的选中的值找出本次取消的项
const { selectedKeys } = this.state;
for (const preKey of selectedKeys) {
if (!currentKeys.includes(preKey)) {
removeKeys.push(preKey);
}
}
return removeKeys?.length ? this.getNewSelectKeys(removeKeys, currentKeys) : currentKeys;
}
public handleTopicChange = (sourceSelectedKeys: string[], targetSelectedKeys: string[]) => {
if (this.selectSingle) {
this.setSelectSingle(false);
} else {
this.getManualSelected(false, [...sourceSelectedKeys, ...targetSelectedKeys])
}
const { topics, targetKeys } = this.state;
// 条件限制只允许选中一边,单向操作
const keys = [...sourceSelectedKeys, ...targetSelectedKeys];
// 判断当前选中项属于哪一类
if (keys.length) {
const isHaRelation = topics.find(item => item.key === keys[0])?.isHaRelation;
const needDisabledKeys = topics.filter(item => item.isHaRelation !== isHaRelation).map(row => row.key);
this.setTopicsStatus(needDisabledKeys, true);
}
const selectedKeys = this.state.selectedKeys.length ? this.getRelatedKeys(keys) : keys;
const isReset = this.isPrimaryStatus(targetKeys);
if (!selectedKeys.length && isReset) {
this.setTopicsStatus([], false, true);
}
this.setState({
selectedKeys,
});
this.getAppRelatedTopicList(selectedKeys);
}
public onDirectChange = (targetKeys: string[], direction: string, moveKeys: string[]) => {
const { primaryStandbyKeys, firstMove, primaryActiveKeys, kafkaUsers } = this.state;
const getKafkaUser = () => {
const newKafkaUsers = kafkaUsers;
const moveTopics = this.getTopicsByKeys(moveKeys);
for (const topic of moveTopics) {
for (const item of newKafkaUsers) {
if (item.selectedTopicNameList.includes(topic)) {
item.show = true;
}
}
}
return newKafkaUsers;
};
// 判断当前移动是否还原为最初的状态
const isReset = this.isPrimaryStatus(targetKeys);
if (firstMove) {
@@ -238,24 +531,26 @@ class TopicHaRelation extends React.Component<IXFormProps> {
this.setTopicsStatus(primaryKeys, true, false);
this.setState(({
firstMove: false,
kafkaUsers: getKafkaUser(),
targetKeys,
}));
return;
}
// 如果是还原为初始状态则还原禁用状态
if (isReset) {
this.setTopicsStatus([], false, true);
this.setState(({
firstMove: true,
targetKeys,
kafkaUsers: [],
}));
return;
}
this.setState({
this.setState(({
targetKeys,
});
kafkaUsers: this.getNewKafkaUser(targetKeys),
}));
}
public componentDidMount() {
@@ -265,17 +560,19 @@ class TopicHaRelation extends React.Component<IXFormProps> {
]).then(([activeRes, standbyRes]: IHaTopic[][]) => {
activeRes = (activeRes || []).map(row => ({
...row,
isHaRelation: row.haRelation === 1 || row.haRelation === 0,
key: row.topicName,
})).filter(item => item.haRelation === null);
})).filter(item => !item.isHaRelation);
standbyRes = (standbyRes || []).map(row => ({
...row,
isHaRelation: row.haRelation === 1 || row.haRelation === 0,
key: row.topicName,
})).filter(item => item.haRelation === 1 || item.haRelation === 0);
})).filter(item => item.isHaRelation);
this.setState({
haTopics: [].concat([...activeRes, ...standbyRes]).sort((a, b) => a.topicName.localeCompare(b.topicName)),
primaryActiveKeys: activeRes.map(row => row.topicName),
primaryStandbyKeys: standbyRes.map(row => row.topicName),
targetKeys: standbyRes.map(row => row.topicName),
topics: [].concat([...activeRes, ...standbyRes]).sort((a, b) => a.topicName.localeCompare(b.topicName)),
primaryActiveKeys: activeRes.map(row => row.key),
primaryStandbyKeys: standbyRes.map(row => row.key),
targetKeys: standbyRes.map(row => row.key),
});
});
}
@@ -287,6 +584,9 @@ class TopicHaRelation extends React.Component<IXFormProps> {
metadata = admin.brokersMetadata ? admin.brokersMetadata : metadata;
let regions = [] as IBrokersRegions[];
regions = admin.brokersRegions ? admin.brokersRegions : regions;
const { kafkaUsers, confirmLoading, radioCheck, targetKeys, selectedKeys, topics, primaryStandbyKeys, spinLoading} = this.state;
const tableData = kafkaUsers.filter(row => row.show);
return (
<>
<Modal
@@ -296,53 +596,78 @@ class TopicHaRelation extends React.Component<IXFormProps> {
onOk={this.handleOk}
onCancel={this.handleCancel}
maskClosable={false}
confirmLoading={this.state.confirmLoading}
width={590}
confirmLoading={confirmLoading}
width={800}
okText="确认"
cancelText="取消"
>
<Alert
message={`将【集群${currentCluster.clusterName}】和【集群${currentCluster.haClusterVO?.clusterName}】的Topic关联高可用关系`}
message={`将【集群${currentCluster.clusterName}】和【集群${currentCluster.haClusterVO?.clusterName}】的Topic关联高可用关系此操作会把同一个kafkaUser下的所有Topic都关联高可用关系`}
type="info"
showIcon={true}
/>
<Form {...layout} name="basic" className="x-form">
{/* <Form.Item label="规则">
{getFieldDecorator('rule', {
initialValue: 'spec',
rules: [{
required: true,
message: '请选择规则',
}],
})(<Radio.Group onChange={this.handleRadioChange} >
<Radio value="all">应用于所有Topic</Radio>
<Radio value="spec">应用于特定Topic</Radio>
</Radio.Group>)}
</Form.Item> */}
{this.state.radioCheck === 'spec' ? <Form.Item className="no-label" label="" >
{getFieldDecorator('topicNames', {
initialValue: this.state.targetKeys,
rules: [{
required: false,
message: '请选择Topic',
}],
})(
<Transfer
className="transfe-list"
dataSource={this.state.haTopics}
targetKeys={this.state.targetKeys}
showSearch={true}
onChange={this.onTransferChange}
render={item => item.topicName}
titles={['未关联', '已关联']}
locale={{
itemUnit: '',
itemsUnit: '',
}}
/>,
)}
</Form.Item> : ''}
</Form>
<Spin spinning={spinLoading}>
<Form {...layout} name="basic" className="x-form">
{/* <Form.Item label="规则">
{getFieldDecorator('rule', {
initialValue: 'spec',
rules: [{
required: true,
message: '请选择规则',
}],
})(<Radio.Group onChange={this.handleRadioChange} >
<Radio value="all">应用于所有Topic</Radio>
<Radio value="spec">应用于特定Topic</Radio>
</Radio.Group>)}
</Form.Item> */}
{radioCheck === 'spec' ? <Form.Item className="no-label" label="" >
{getFieldDecorator('topicNames', {
initialValue: targetKeys,
rules: [{
required: false,
message: '请选择Topic',
}],
})(
<TransferTable
selectedKeys={selectedKeys}
topicChange={this.handleTopicChange}
onDirectChange={this.onDirectChange}
columns={columns}
dataSource={topics}
currentCluster={currentCluster}
getManualSelected={this.getManualSelected}
transferAttrs={{
titles: ['未关联', '已关联'],
}}
tableAttrs={{
className: 'no-table-header',
}}
/>,
)}
</Form.Item> : null}
{radioCheck === 'spec' ? <Table
className="modal-table-content no-lr-padding"
columns={kafkaUserColumn}
dataSource={tableData}
size="small"
rowKey="kafkaUser"
pagination={false}
scroll={{ y: 300 }}
/> : null}
{targetKeys.length < primaryStandbyKeys.length ? <Form.Item label="数据清理策略" labelCol={{span: 4}} wrapperCol={{span: 20}}>
{getFieldDecorator('retainStandbyResource', {
initialValue: false,
rules: [{
required: true,
message: '请选择数据清理策略',
}],
})(<Radio.Group>
<Radio value={false}></Radio>
<Radio value={true}></Radio>
</Radio.Group>)}
</Form.Item> : null}
</Form>
</Spin>
</Modal>
</>
);

View File

@@ -1,12 +1,13 @@
import * as React from 'react';
import { admin } from 'store/admin';
import { Modal, Form, Radio, Tag, Popover, Button } from 'antd';
import { Modal, Form, Radio, Tag, Popover, Button, Tooltip, Spin } from 'antd';
import { IBrokersMetadata, IBrokersRegions, IMetaData } from 'types/base-type';
import { Alert, Icon, message, Table, Transfer } from 'component/antd';
import { getClusterHaTopics, getAppRelatedTopics, createSwitchTask } from 'lib/api';
import { TooltipPlacement } from 'antd/es/tooltip';
import * as XLSX from 'xlsx';
import moment from 'moment';
import { cloneDeep } from "lodash";
import { timeMinute } from 'constants/strategy';
const layout = {
@@ -35,13 +36,17 @@ interface IHaTopic {
disabled?: boolean;
}
interface IKafkaUser {
export interface IKafkaUser {
clusterPhyId: number;
kafkaUser: string;
notHaTopicNameList: string[];
notSelectTopicNameList: string[];
selectedTopicNameList: string[];
show: boolean;
manualSelectedTopics: string[];
autoSelectedTopics: string[];
clientId?: string;
haClientIdList?: string[]
}
const columns = [
@@ -71,17 +76,23 @@ const kafkaUserColumn = [
ellipsis: true,
},
{
dataIndex: 'selectedTopicNameList',
dataIndex: 'clientId',
title: 'clientID',
width: 100,
ellipsis: true,
},
{
dataIndex: 'manualSelectedTopics',
title: '已选中Topic',
width: 120,
// width: 120,
render: (text: string[]) => {
return text?.length ? renderAttributes({ data: text, limit: 3 }) : '-';
},
},
{
dataIndex: 'notSelectTopicNameList',
dataIndex: 'autoSelectedTopics',
title: '选中关联Topic',
width: 120,
// width: 120,
render: (text: string[]) => {
return text?.length ? renderAttributes({ data: text, limit: 3 }) : '-';
},
@@ -89,7 +100,7 @@ const kafkaUserColumn = [
{
dataIndex: 'notHaTopicNameList',
title: '未建立HA Topic',
width: 120,
// width: 120,
render: (text: string[]) => {
return text?.length ? renderAttributes({ data: text, limit: 3 }) : '-';
},
@@ -135,31 +146,64 @@ export const renderAttributes = (params: {
class TopicHaSwitch extends React.Component<IXFormProps> {
public state = {
radioCheck: 'spec',
switchMode: 'kafkaUser',
targetKeys: [] as string[],
selectedKeys: [] as string[],
topics: [] as IHaTopic[],
kafkaUsers: [] as IKafkaUser[],
primaryTopics: [] as string[],
primaryActiveKeys: [] as string[],
primaryStandbyKeys: [] as string[],
firstMove: true,
manualSelectedKeys: [] as string[],
selectTableColumn: kafkaUserColumn.filter(item => item.title !== 'clientID') as [],
spinLoading: false,
};
public selectSingle = null as boolean;
public manualSelectedNames = [] as string[];
public setSelectSingle = (val: boolean) => {
this.selectSingle = val;
}
public setManualSelectedNames = (keys: string[]) => {
// this.manualSelectedNames = this.getTopicsByKeys(keys);
this.manualSelectedNames = keys;
}
public filterManualSelectedKeys = (key: string, selected: boolean) => {
const newManualSelectedKeys = [...this.state.manualSelectedKeys];
const index = newManualSelectedKeys.findIndex(item => item === key);
if (selected) {
if (index === -1) newManualSelectedKeys.push(key);
} else {
if (index !== -1) newManualSelectedKeys.splice(index, 1);
}
this.setManualSelectedNames(newManualSelectedKeys);
this.setState({
manualSelectedKeys: newManualSelectedKeys,
});
}
public getManualSelected = (single: boolean, key?: any, selected?: boolean) => {
this.setSelectSingle(single);
if (single) {
this.filterManualSelectedKeys(key, selected);
} else {
this.setManualSelectedNames(key);
this.setState({
manualSelectedKeys: key,
});
}
}
public isPrimaryStatus = (targetKeys: string[]) => {
const { primaryStandbyKeys } = this.state;
let isReset = false;
// 判断当前移动是否还原为最初的状态
if (primaryStandbyKeys.length === targetKeys.length) {
targetKeys.sort((a, b) => +a - (+b));
primaryStandbyKeys.sort((a, b) => +a - (+b));
let i = 0;
while (i < targetKeys.length) {
if (targetKeys[i] === primaryStandbyKeys[i]) {
i++;
} else {
break;
}
}
isReset = i === targetKeys.length;
const diff = targetKeys.find(item => primaryStandbyKeys.indexOf(item) < 0);
isReset = diff ? false : true;
}
return isReset;
}
@@ -168,16 +212,17 @@ class TopicHaSwitch extends React.Component<IXFormProps> {
const targetTopics = [];
for (const key of currentKeys) {
if (!primaryKeys.includes(key)) {
const topic = this.state.topics.find(item => item.key === key)?.topicName;
targetTopics.push(topic);
// const topic = this.state.topics.find(item => item.key === key)?.topicName;
// targetTopics.push(topic);
targetTopics.push(key);
}
}
return targetTopics;
}
public handleOk = () => {
const { primaryStandbyKeys, primaryActiveKeys, topics } = this.state;
const standbyClusterId = this.props.currentCluster.haClusterVO.clusterId;
const { primaryStandbyKeys, primaryActiveKeys, topics, kafkaUsers, switchMode } = this.state;
const standbyClusterId = this.props.currentCluster.haClusterVO?.clusterId;
const activeClusterId = this.props.currentCluster.clusterId;
this.props.form.validateFields((err: any, values: any) => {
@@ -188,6 +233,7 @@ class TopicHaSwitch extends React.Component<IXFormProps> {
all: true,
mustContainAllKafkaUserTopics: true,
standbyClusterPhyId: standbyClusterId,
kafkaUserAndClientIdList: [],
topicNameList: [],
}).then(res => {
message.success('任务创建成功');
@@ -217,11 +263,28 @@ class TopicHaSwitch extends React.Component<IXFormProps> {
const activeClusterPhyId = currentStandbyKeys.length > primaryStandbyKeys.length ? standbyClusterId : activeClusterId;
const standbyClusterPhyId = currentStandbyKeys.length > primaryStandbyKeys.length ? activeClusterId : standbyClusterId;
const targetTopics = this.getTargetTopics(currentKeys, primaryKeys);
const clientIdParams = kafkaUsers.map(item => ({ clientId: item.clientId, kafkaUser: item.kafkaUser }));
const kafkaUserParams = [] as any;
kafkaUsers.forEach(item => {
kafkaUserParams.push({
clientId: null,
kafkaUser: item.kafkaUser,
});
if (item.haClientIdList?.length) {
item.haClientIdList.forEach(clientId => {
kafkaUserParams.push({
clientId,
kafkaUser: item.kafkaUser,
});
});
}
});
createSwitchTask({
activeClusterPhyId,
all: false,
mustContainAllKafkaUserTopics: true,
mustContainAllKafkaUserTopics: switchMode === 'kafkaUser',
standbyClusterPhyId,
kafkaUserAndClientIdList: switchMode === 'clientID' ? clientIdParams : kafkaUserParams,
topicNameList: targetTopics,
}).then(res => {
message.success('任务创建成功');
@@ -252,8 +315,7 @@ class TopicHaSwitch extends React.Component<IXFormProps> {
const topicName = topics.find(row => row.key === key)?.topicName;
for (const item of kafkaUsers) {
if (item.selectedTopicNameList.includes(topicName)) {
relatedTopics = relatedTopics.concat(item.selectedTopicNameList);
relatedTopics = relatedTopics.concat(item.notSelectTopicNameList);
relatedTopics = relatedTopics.concat(item.selectedTopicNameList, item.notSelectTopicNameList);
}
}
for (const item of relatedTopics) {
@@ -291,21 +353,20 @@ class TopicHaSwitch extends React.Component<IXFormProps> {
}));
}
public getFilterTopics = (selectKeys: string[]) => {
public getTopicsByKeys = (keys: string[]) => {
// 依据key值找topicName
const filterTopics: string[] = [];
const targetKeys = selectKeys;
for (const key of targetKeys) {
const topicNames: string[] = [];
for (const key of keys) {
const topicName = this.state.topics.find(item => item.key === key)?.topicName;
if (topicName) {
filterTopics.push(topicName);
topicNames.push(topicName);
}
}
return filterTopics;
return topicNames;
}
public getNewKafkaUser = (targetKeys: string[]) => {
const { primaryStandbyKeys, topics } = this.state;
const { primaryStandbyKeys, kafkaUsers, topics } = this.state;
const removeKeys = [];
const addKeys = [];
for (const key of primaryStandbyKeys) {
@@ -321,9 +382,9 @@ class TopicHaSwitch extends React.Component<IXFormProps> {
}
}
const keepKeys = [...removeKeys, ...addKeys];
const newKafkaUsers = this.state.kafkaUsers;
const newKafkaUsers = kafkaUsers;
const moveTopics = this.getFilterTopics(keepKeys);
const moveTopics = this.getTopicsByKeys(keepKeys);
for (const topic of moveTopics) {
for (const item of newKafkaUsers) {
@@ -355,8 +416,8 @@ class TopicHaSwitch extends React.Component<IXFormProps> {
}
public getAppRelatedTopicList = (selectedKeys: string[]) => {
const { topics, targetKeys, primaryStandbyKeys, kafkaUsers } = this.state;
const filterTopicNameList = this.getFilterTopics(selectedKeys);
const { topics, targetKeys, primaryStandbyKeys, kafkaUsers, switchMode } = this.state;
const filterTopicNameList = this.getTopicsByKeys(selectedKeys);
const isReset = this.isPrimaryStatus(targetKeys);
if (!filterTopicNameList.length && isReset) {
@@ -376,10 +437,14 @@ class TopicHaSwitch extends React.Component<IXFormProps> {
}
// 单向选择所以取当前值的aactiveClusterId
const clusterPhyId = topics.find(item => item.topicName === filterTopicNameList[0]).activeClusterId;
const clusterPhyId = topics.find(item => item.topicName === filterTopicNameList[0])?.activeClusterId;
if (!clusterPhyId) return;
this.setState({spinLoading: true});
getAppRelatedTopics({
clusterPhyId,
filterTopicNameList,
ha: true,
useKafkaUserAndClientId: switchMode === 'clientID',
}).then((res: IKafkaUser[]) => {
let notSelectTopicNames: string[] = [];
const notSelectTopicKeys: string[] = [];
@@ -390,7 +455,7 @@ class TopicHaSwitch extends React.Component<IXFormProps> {
for (const item of notSelectTopicNames) {
const key = topics.find(row => row.topicName === item)?.key;
if (key) {
if (key && notSelectTopicKeys.indexOf(key) < 0) {
notSelectTopicKeys.push(key);
}
}
@@ -399,11 +464,13 @@ class TopicHaSwitch extends React.Component<IXFormProps> {
const newKafkaUsers = (res || []).map(item => ({
...item,
show: true,
manualSelectedTopics: item.selectedTopicNameList.filter(topic => this.manualSelectedNames.indexOf(topic) > -1),
autoSelectedTopics: [...item.selectedTopicNameList, ...item.notSelectTopicNameList].filter(topic => this.manualSelectedNames.indexOf(topic) === -1),
}));
const { kafkaUsers } = this.state;
for (const item of kafkaUsers) {
const resItem = res.find(row => row.kafkaUser === item.kafkaUser);
const resItem = res.find(row => switchMode === 'clientID' ? row.kafkaUser === item.kafkaUser && row.clientId === item.clientId : row.kafkaUser === item.kafkaUser);
if (!resItem) {
newKafkaUsers.push(item);
}
@@ -416,6 +483,8 @@ class TopicHaSwitch extends React.Component<IXFormProps> {
if (notSelectTopicKeys.length) {
this.getAppRelatedTopicList(newSelectedKeys);
}
}).finally(() => {
this.setState({spinLoading: false});
});
}
@@ -440,7 +509,7 @@ class TopicHaSwitch extends React.Component<IXFormProps> {
// 判断当前选中项属于哪一类
if (keys.length) {
const activeClusterId = topics.find(item => item.key === keys[0]).activeClusterId;
const activeClusterId = topics.find(item => item.key === keys[0])?.activeClusterId;
const needDisabledKeys = topics.filter(item => item.activeClusterId !== activeClusterId).map(row => row.key);
this.setTopicsStatus(needDisabledKeys, true);
}
@@ -457,11 +526,11 @@ class TopicHaSwitch extends React.Component<IXFormProps> {
}
public onDirectChange = (targetKeys: string[], direction: string, moveKeys: string[]) => {
const { primaryStandbyKeys, firstMove, primaryActiveKeys, topics } = this.state;
const { primaryStandbyKeys, firstMove, primaryActiveKeys, kafkaUsers, topics } = this.state;
const getKafkaUser = () => {
const newKafkaUsers = this.state.kafkaUsers;
const moveTopics = this.getFilterTopics(moveKeys);
const newKafkaUsers = kafkaUsers;
const moveTopics = this.getTopicsByKeys(moveKeys);
for (const topic of moveTopics) {
for (const item of newKafkaUsers) {
if (item.selectedTopicNameList.includes(topic)) {
@@ -503,21 +572,27 @@ class TopicHaSwitch extends React.Component<IXFormProps> {
}
public downloadData = () => {
const { kafkaUsers } = this.state;
const { kafkaUsers, switchMode } = this.state;
const tableData = kafkaUsers.map(item => {
return {
const column = {
// tslint:disable
'kafkaUser': item.kafkaUser,
'已选中Topic': item.selectedTopicNameList?.join('、'),
'选中关联Topic': item.notSelectTopicNameList?.join('、'),
'clientID': item.clientId,
'选中Topic': item.manualSelectedTopics?.join('、'),
'选中关联Topic': item.autoSelectedTopics?.join('、'),
'未建立HA Topic': item.notHaTopicNameList?.join(``),
};
if (switchMode === 'kafkaUser') {
delete column.clientID
}
return column;
});
const data = [].concat(tableData);
const wb = XLSX.utils.book_new();
// json转sheet
const header = ['kafkaUser', 'clientID', '已选中Topic', '选中关联Topic', '未建立HA Topic'];
const ws = XLSX.utils.json_to_sheet(data, {
header: ['kafkaUser', '已选中Topic', '选中关联Topic', '未建立HA Topic'],
header: switchMode === 'kafkaUser' ? header.filter(item => item !== 'clientID') : header,
});
// XLSX.utils.
XLSX.utils.book_append_sheet(wb, ws, 'kafkaUser');
@@ -537,18 +612,38 @@ class TopicHaSwitch extends React.Component<IXFormProps> {
return false;
}
public onModeChange = (e: any) => {
const mode = e.target.value;
// 切换方式变更时,初始化数据
const { primaryTopics, primaryStandbyKeys } = this.state;
this.setState({
switchMode: mode,
topics: cloneDeep(primaryTopics),
targetKeys: primaryStandbyKeys,
selectedKeys: [],
kafkaUsers: [],
firstMove: true,
manualSelectedKeys: [],
selectTableColumn: mode === 'kafkaUser' ? kafkaUserColumn.filter(item => item.title !== 'clientID') : kafkaUserColumn,
});
this.props.form.setFieldsValue({targetKeys: primaryStandbyKeys});
this.setSelectSingle(null);
this.setManualSelectedNames([]);
}
public componentDidMount() {
const standbyClusterId = this.props.currentCluster.haClusterVO.clusterId;
const standbyClusterId = this.props.currentCluster.haClusterVO?.clusterId;
const activeClusterId = this.props.currentCluster.clusterId;
getClusterHaTopics(this.props.currentCluster.clusterId, standbyClusterId).then((res: IHaTopic[]) => {
res = res.map((item, index) => ({
key: index.toString(),
getClusterHaTopics(activeClusterId, standbyClusterId).then((res: IHaTopic[]) => {
res = res.map((item) => ({
key: item.topicName,
...item,
}));
const targetKeys = (res || []).filter((item) => item.activeClusterId === standbyClusterId).map(row => row.key);
const primaryActiveKeys = (res || []).filter((item) => item.activeClusterId === activeClusterId).map(row => row.key);
this.setState({
topics: res || [],
primaryTopics: cloneDeep(res) || [],
primaryStandbyKeys: targetKeys,
primaryActiveKeys,
targetKeys,
@@ -563,7 +658,15 @@ class TopicHaSwitch extends React.Component<IXFormProps> {
metadata = admin.brokersMetadata ? admin.brokersMetadata : metadata;
let regions = [] as IBrokersRegions[];
regions = admin.brokersRegions ? admin.brokersRegions : regions;
const tableData = this.state.kafkaUsers.filter(row => row.show);
const { switchMode, kafkaUsers, radioCheck, targetKeys, selectedKeys, topics, selectTableColumn, spinLoading } = this.state;
const tableData = kafkaUsers.filter(row => row.show);
const rulesNode = (
<div>
1ClientID格式为P#C#
<br />
2ClientID格式的高可用TopicTopicClientID的格式是否正确
</div>
);
return (
<Modal
@@ -580,62 +683,83 @@ class TopicHaSwitch extends React.Component<IXFormProps> {
}
>
<Alert
message={`注意:必须把同一个kafkauser关联的所有Topic都建立高可用关系并且都选中才能执行任务`}
message={`注意:必须把同一个${switchMode}关联的所有Topic都建立高可用关系并且都选中才能执行任务`}
type="info"
showIcon={true}
/>
<Form {...layout} name="basic" className="x-form">
{/* <Form.Item label="规则" >
{getFieldDecorator('rule', {
initialValue: 'spec',
rules: [{
required: true,
message: '请选择规则',
}],
})(<Radio.Group onChange={this.handleRadioChange} >
<Radio value="all">应用于所有Topic</Radio>
<Radio value="spec">应用于特定Topic</Radio>
</Radio.Group>)}
</Form.Item> */}
{this.state.radioCheck === 'spec' ? <Form.Item className="no-label" label="" >
{getFieldDecorator('targetKeys', {
initialValue: this.state.targetKeys,
rules: [{
required: false,
message: '请选择Topic',
}],
})(
<TransferTable
selectedKeys={this.state.selectedKeys}
topicChange={this.handleTopicChange}
onDirectChange={this.onDirectChange}
dataSource={this.state.topics}
currentCluster={currentCluster}
/>,
)}
</Form.Item> : ''}
</Form>
{this.state.radioCheck === 'spec' ?
<>
<Table
className="modal-table-content"
columns={kafkaUserColumn}
dataSource={tableData}
size="small"
rowKey="kafkaUser"
pagination={false}
scroll={{ y: 300 }}
/>
{this.state.kafkaUsers.length ? <div onClick={this.downloadData} className="modal-table-download"><a></a></div> : null}
</>
: null}
<Spin spinning={spinLoading}>
<Form {...layout} name="basic" className="x-form">
<Form.Item label="切换维度">
{getFieldDecorator('switchMode', {
initialValue: 'kafkaUser',
rules: [{
required: true,
message: '请选择切换维度',
}],
})(<Radio.Group onChange={this.onModeChange}>
<Radio value="kafkaUser">kafkaUser</Radio>
<Radio value="clientID">kafkaUser + clientID</Radio>
</Radio.Group>)}
</Form.Item>
{/* <Form.Item label="规则" >
{getFieldDecorator('rule', {
initialValue: 'spec',
rules: [{
required: true,
message: '请选择规则',
}],
})(<Radio.Group onChange={this.handleRadioChange} >
<Radio value="all">应用于所有Topic</Radio>
<Radio value="spec">应用于特定Topic</Radio>
</Radio.Group>)}
</Form.Item> */}
{switchMode === 'clientID' && <div style={{ margin: '-10px 0 10px 0' }} >
<Tooltip placement="bottomLeft" title={rulesNode}>
<a></a>
</Tooltip>
</div>}
{radioCheck === 'spec' ? <Form.Item className="no-label" label="" >
{getFieldDecorator('targetKeys', {
initialValue: targetKeys,
rules: [{
required: false,
message: '请选择Topic',
}],
})(
<TransferTable
selectedKeys={selectedKeys}
topicChange={this.handleTopicChange}
onDirectChange={this.onDirectChange}
columns={columns}
dataSource={topics}
currentCluster={currentCluster}
getManualSelected={this.getManualSelected}
/>,
)}
</Form.Item> : null}
</Form>
{radioCheck === 'spec' ?
<>
<Table
className="modal-table-content"
columns={selectTableColumn}
dataSource={tableData}
size="small"
rowKey="kafkaUser"
pagination={false}
scroll={{ y: 300 }}
/>
{tableData.length ? <div onClick={this.downloadData} className="modal-table-download"><a></a></div> : null}
</>
: null}
</Spin>
</Modal>
);
}
}
export const TopicSwitchWrapper = Form.create<IXFormProps>()(TopicHaSwitch);
const TableTransfer = ({ leftColumns, ...restProps }: any) => (
export const TableTransfer = ({ leftColumns, getManualSelected, tableAttrs, ...restProps }: any) => (
<Transfer {...restProps} showSelectAll={true}>
{({
filteredItems,
@@ -651,6 +775,7 @@ const TableTransfer = ({ leftColumns, ...restProps }: any) => (
disabled: item.disabled,
}),
onSelect({ key }: any, selected: any) {
getManualSelected(true, key, selected);
onItemSelect(key, selected);
},
selectedRowKeys: listSelectedKeys,
@@ -668,9 +793,11 @@ const TableTransfer = ({ leftColumns, ...restProps }: any) => (
onRow={({ key, disabled }) => ({
onClick: () => {
if (disabled) return;
getManualSelected(true, key, listSelectedKeys.includes(key));
onItemSelect(key, !listSelectedKeys.includes(key));
},
})}
{...tableAttrs}
/>
);
}}
@@ -683,8 +810,12 @@ interface IProps {
onDirectChange?: any;
currentCluster: any;
topicChange: any;
columns: any[];
dataSource: any[];
selectedKeys: string[];
getManualSelected: any;
transferAttrs?: any;
tableAttrs?: any;
}
export class TransferTable extends React.Component<IProps> {
@@ -695,7 +826,7 @@ export class TransferTable extends React.Component<IProps> {
}
public render() {
const { currentCluster, dataSource, value, topicChange, selectedKeys } = this.props;
const { currentCluster, columns, dataSource, value, topicChange, selectedKeys, getManualSelected, transferAttrs, tableAttrs } = this.props;
return (
<div>
<TableTransfer
@@ -705,12 +836,16 @@ export class TransferTable extends React.Component<IProps> {
showSearch={true}
onChange={this.onChange}
onSelectChange={topicChange}
filterOption={(inputValue: string, item: any) => item.topicName?.indexOf(inputValue) > -1}
leftColumns={columns}
titles={[`集群${currentCluster.clusterName}`, `集群${currentCluster.haClusterVO.clusterName}`]}
titles={[`集群${currentCluster.clusterName}`, `集群${currentCluster.haClusterVO?.clusterName}`]}
locale={{
itemUnit: '',
itemsUnit: '',
}}
getManualSelected={getManualSelected}
tableAttrs={tableAttrs}
{...transferAttrs}
/>
</div>
);

View File

@@ -0,0 +1,8 @@
.ant-table-wrapper.no-lr-padding {
padding-left: 0!important;
padding-right: 0!important;
}
.no-table-header .ant-table-header {
display: none;
}

View File

@@ -6,6 +6,8 @@ import { Table, Tooltip } from 'component/antd';
import { SearchAndFilterContainer } from 'container/search-filter';
import Url from 'lib/url-parser';
import { pagination, cellStyle } from 'constants/table';
import moment = require('moment');
import { timeFormat } from 'constants/strategy';
@observer
export class ConnectInformation extends SearchAndFilterContainer {
@@ -27,44 +29,70 @@ export class ConnectInformation extends SearchAndFilterContainer {
title: '客户端类型',
dataIndex: 'clientType',
key: 'clientType',
width: '20%',
width: 130,
filters: [{ text: '消费', value: 'consumer' }, { text: '生产', value: 'produce' }],
onFilter: (value: string, record: IConnectionInfo) => record.clientType.indexOf(value) === 0,
render: (t: string) =>
<span>{t === 'consumer' ? '消费' : '生产'}</span>,
}, this.renderColumnsFilter('filterVisible'));
const columns = [{
title: 'AppID',
dataIndex: 'appId',
key: 'appId',
width: '20%',
sorter: (a: IConnectionInfo, b: IConnectionInfo) => a.appId.charCodeAt(0) - b.appId.charCodeAt(0),
},
{
title: '主机名',
dataIndex: 'hostname',
key: 'hostname',
width: '40%',
onCell: () => ({
style: {
maxWidth: 250,
...cellStyle,
},
}),
render: (t: string) => {
return (
<Tooltip placement="bottomLeft" title={t} >{t}</Tooltip>
);
const columns = [
{
title: 'AppID',
dataIndex: 'appId',
key: 'appId',
width: '30%',
sorter: (a: IConnectionInfo, b: IConnectionInfo) => a.appId.charCodeAt(0) - b.appId.charCodeAt(0),
},
{
title: 'clientID',
dataIndex: 'clientId',
key: 'clientId',
width: '30%',
onCell: () => ({
style: {
maxWidth: 250,
...cellStyle,
},
}),
render: (t: string) => {
return (
<Tooltip placement="bottomLeft" title={t} >{t}</Tooltip>
);
},
},
{
title: '主机名',
dataIndex: 'hostname',
key: 'hostname',
width: '30%',
onCell: () => ({
style: {
maxWidth: 250,
...cellStyle,
},
}),
render: (t: string) => {
return (
<Tooltip placement="bottomLeft" title={t} >{t}</Tooltip>
);
},
},
{
title: '客户端版本',
dataIndex: 'clientVersion',
key: 'clientVersion',
width: 130,
},
},
{
title: '客户端版本',
dataIndex: 'clientVersion',
key: 'clientVersion',
width: '20%',
},
clientType,
{
title: '最后访问时间',
dataIndex: 'realConnectTime',
key: 'realConnectTime',
width: 170,
render: (t: number) => moment(t).format(timeFormat),
sorter: (a: IConnectionInfo, b: IConnectionInfo) => a.realConnectTime - b.realConnectTime,
},
];
if (connectInfo) {
return (

View File

@@ -75,6 +75,8 @@ export interface IConnectionInfo {
hostname: string;
ip: string;
topicName: string;
clientId?: string;
realConnectTime?: number;
key?: number;
}

View File

@@ -130,7 +130,11 @@ module.exports = {
historyApiFallback: true,
proxy: {
'/api/v1/': {
target: 'http://127.0.0.1:8080/',
// target: 'http://117.51.150.133:8080/',
target: 'http://10.190.55.249:8080/',
// target: 'http://10.179.37.199:8008',
// target: 'http://10.179.148.210:8080',
// target: 'http://99.11.45.164:8888',
changeOrigin: true,
}
},