mirror of
https://github.com/apache/rocketmq-dashboard.git
synced 2025-09-10 03:29:59 +08:00
[GSOC][RIP-78][ISSUES#308] Add part of refactored front-end files (#311)
This commit is contained in:
127
frontend-new/src/components/acl/ResourceInput.jsx
Normal file
127
frontend-new/src/components/acl/ResourceInput.jsx
Normal file
@@ -0,0 +1,127 @@
|
||||
/*
|
||||
* Licensed to the Apache Software Foundation (ASF) under one or more
|
||||
* contributor license agreements. See the NOTICE file distributed with
|
||||
* this work for additional information regarding copyright ownership.
|
||||
* The ASF licenses this file to You under the Apache License, Version 2.0
|
||||
* (the "License"); you may not use this file except in compliance with
|
||||
* the License. You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import { Input, Select, Tag, Space } from 'antd';
|
||||
import { PlusOutlined } from '@ant-design/icons';
|
||||
import React, { useState } from 'react';
|
||||
|
||||
const { Option } = Select;
|
||||
|
||||
// 资源类型枚举
|
||||
const resourceTypes = [
|
||||
{ value: 0, label: 'Unknown', prefix: 'UNKNOWN' },
|
||||
{ value: 1, label: 'Any', prefix: 'ANY' },
|
||||
{ value: 2, label: 'Cluster', prefix: 'CLUSTER' },
|
||||
{ value: 3, label: 'Namespace', prefix: 'NAMESPACE' },
|
||||
{ value: 4, label: 'Topic', prefix: 'TOPIC' },
|
||||
{ value: 5, label: 'Group', prefix: 'GROUP' },
|
||||
];
|
||||
|
||||
const ResourceInput = ({ value = [], onChange }) => {
|
||||
// 确保 value 始终是数组
|
||||
const safeValue = Array.isArray(value) ? value : [];
|
||||
|
||||
const [selectedType, setSelectedType] = useState(resourceTypes[0].prefix); // 默认选中第一个
|
||||
const [resourceName, setResourceName] = useState('');
|
||||
const [inputVisible, setInputVisible] = useState(false);
|
||||
const inputRef = React.useRef(null);
|
||||
|
||||
// 处理删除已添加的资源
|
||||
const handleClose = removedResource => {
|
||||
const newResources = safeValue.filter(resource => resource !== removedResource);
|
||||
onChange(newResources);
|
||||
};
|
||||
|
||||
// 显示输入框
|
||||
const showInput = () => {
|
||||
setInputVisible(true);
|
||||
setTimeout(() => {
|
||||
inputRef.current?.focus();
|
||||
}, 0);
|
||||
};
|
||||
|
||||
// 处理资源类型选择
|
||||
const handleTypeChange = type => {
|
||||
setSelectedType(type);
|
||||
};
|
||||
|
||||
// 处理资源名称输入
|
||||
const handleNameChange = e => {
|
||||
setResourceName(e.target.value);
|
||||
};
|
||||
|
||||
// 添加资源到列表
|
||||
const handleAddResource = () => {
|
||||
if (resourceName) {
|
||||
const fullResource = `${selectedType}:${resourceName}`;
|
||||
// 避免重复添加
|
||||
if (!safeValue.includes(fullResource)) {
|
||||
onChange([...safeValue, fullResource]);
|
||||
}
|
||||
setResourceName(''); // 清空输入
|
||||
setInputVisible(false); // 隐藏输入框
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<Space size={[0, 8]} wrap>
|
||||
{/* 显示已添加的资源标签 */}
|
||||
{safeValue.map(resource => ( // 使用 safeValue
|
||||
<Tag
|
||||
key={resource}
|
||||
closable
|
||||
onClose={() => handleClose(resource)}
|
||||
color="blue"
|
||||
>
|
||||
{resource}
|
||||
</Tag>
|
||||
))}
|
||||
|
||||
{/* 新增资源输入区域 */}
|
||||
{inputVisible ? (
|
||||
<Space>
|
||||
<Select
|
||||
value={selectedType}
|
||||
style={{ width: 120 }}
|
||||
onChange={handleTypeChange}
|
||||
>
|
||||
{resourceTypes.map(type => (
|
||||
<Option key={type.value} value={type.prefix}>
|
||||
{type.label}
|
||||
</Option>
|
||||
))}
|
||||
</Select>
|
||||
<Input
|
||||
ref={inputRef}
|
||||
style={{ width: 180 }}
|
||||
value={resourceName}
|
||||
onChange={handleNameChange}
|
||||
onPressEnter={handleAddResource}
|
||||
onBlur={handleAddResource} // 失去焦点也自动添加
|
||||
placeholder="请输入资源名称"
|
||||
/>
|
||||
</Space>
|
||||
) : (
|
||||
<Tag onClick={showInput} style={{ background: '#fff', borderStyle: 'dashed' }}>
|
||||
<PlusOutlined /> 添加资源
|
||||
</Tag>
|
||||
)}
|
||||
</Space>
|
||||
);
|
||||
};
|
||||
|
||||
export default ResourceInput;
|
101
frontend-new/src/components/acl/SubjectInput.jsx
Normal file
101
frontend-new/src/components/acl/SubjectInput.jsx
Normal file
@@ -0,0 +1,101 @@
|
||||
/*
|
||||
* Licensed to the Apache Software Foundation (ASF) under one or more
|
||||
* contributor license agreements. See the NOTICE file distributed with
|
||||
* this work for additional information regarding copyright ownership.
|
||||
* The ASF licenses this file to You under the Apache License, Version 2.0
|
||||
* (the "License"); you may not use this file except in compliance with
|
||||
* the License. You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import { Input, Select } from 'antd';
|
||||
import React, { useState, useEffect } from 'react';
|
||||
|
||||
const { Option } = Select;
|
||||
|
||||
// Subject 类型枚举
|
||||
const subjectTypes = [
|
||||
{ value: 'User', label: 'User' },
|
||||
];
|
||||
|
||||
const SubjectInput = ({ value, onChange, disabled }) => {
|
||||
// 解析传入的 value,将其拆分为 type 和 name
|
||||
const parseValue = (val) => {
|
||||
if (!val || typeof val !== 'string') {
|
||||
return { type: subjectTypes[0].value, name: '' }; // 默认值
|
||||
}
|
||||
const parts = val.split(':');
|
||||
if (parts.length === 2 && subjectTypes.some(t => t.value === parts[0])) {
|
||||
return { type: parts[0], name: parts[1] };
|
||||
}
|
||||
return { type: subjectTypes[0].value, name: val }; // 如果格式不匹配,将整个值作为 name,类型设为默认
|
||||
};
|
||||
|
||||
const [currentType, setCurrentType] = useState(() => parseValue(value).type);
|
||||
const [currentName, setCurrentName] = useState(() => parseValue(value).name);
|
||||
|
||||
// 当外部 value 变化时,更新内部状态
|
||||
useEffect(() => {
|
||||
const parsed = parseValue(value);
|
||||
setCurrentType(parsed.type);
|
||||
setCurrentName(parsed.name);
|
||||
}, [value]);
|
||||
|
||||
// 当类型或名称变化时,通知 Form.Item
|
||||
const triggerChange = (changedType, changedName) => {
|
||||
if (onChange) {
|
||||
// 只有当名称不为空时才组合,否则只返回类型或空字符串
|
||||
if (changedName) {
|
||||
onChange(`${changedType}:${changedName}`);
|
||||
} else if (changedType) { // 如果只选择了类型,但名称为空,则不组合
|
||||
onChange(''); // 或者根据需求返回 'User:' 等,但通常这种情况下不应该有值
|
||||
} else {
|
||||
onChange('');
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const onTypeChange = (newType) => {
|
||||
setCurrentType(newType);
|
||||
triggerChange(newType, currentName);
|
||||
};
|
||||
|
||||
const onNameChange = (e) => {
|
||||
const newName = e.target.value;
|
||||
setCurrentName(newName);
|
||||
triggerChange(currentType, newName);
|
||||
};
|
||||
|
||||
return (
|
||||
<Input.Group compact>
|
||||
<Select
|
||||
style={{ width: '30%' }}
|
||||
value={currentType}
|
||||
onChange={onTypeChange}
|
||||
disabled={disabled}
|
||||
>
|
||||
{subjectTypes.map(type => (
|
||||
<Option key={type.value} value={type.value}>
|
||||
{type.label}
|
||||
</Option>
|
||||
))}
|
||||
</Select>
|
||||
<Input
|
||||
style={{ width: '70%' }}
|
||||
value={currentName}
|
||||
onChange={onNameChange}
|
||||
placeholder="请输入名称 (例如: yourUsername)"
|
||||
disabled={disabled}
|
||||
/>
|
||||
</Input.Group>
|
||||
);
|
||||
};
|
||||
|
||||
export default SubjectInput;
|
103
frontend-new/src/components/consumer/ClientInfoModal.jsx
Normal file
103
frontend-new/src/components/consumer/ClientInfoModal.jsx
Normal file
@@ -0,0 +1,103 @@
|
||||
/*
|
||||
* Licensed to the Apache Software Foundation (ASF) under one or more
|
||||
* contributor license agreements. See the NOTICE file distributed with
|
||||
* this work for additional information regarding copyright ownership.
|
||||
* The ASF licenses this file to You under the Apache License, Version 2.0
|
||||
* (the "License"); you may not use this file except in compliance with
|
||||
* the License. You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import { Modal, Table, Spin } from 'antd';
|
||||
import { remoteApi } from '../../api/remoteApi/remoteApi';
|
||||
import { useLanguage } from '../../i18n/LanguageContext';
|
||||
|
||||
const ClientInfoModal = ({ visible, group, address, onCancel }) => {
|
||||
const { t } = useLanguage();
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [connectionData, setConnectionData] = useState(null);
|
||||
const [subscriptionData, setSubscriptionData] = useState(null);
|
||||
|
||||
useEffect(() => {
|
||||
const fetchData = async () => {
|
||||
if (!visible) return;
|
||||
|
||||
setLoading(true);
|
||||
try {
|
||||
const connResponse = await remoteApi.queryConsumerConnection(group, address);
|
||||
const topicResponse = await remoteApi.queryTopicByConsumer(group, address);
|
||||
|
||||
if (connResponse.status === 0) setConnectionData(connResponse.data);
|
||||
if (topicResponse.status === 0) setSubscriptionData(topicResponse.data);
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
fetchData();
|
||||
}, [visible, group, address]);
|
||||
|
||||
const connectionColumns = [
|
||||
{ title: 'ClientId', dataIndex: 'clientId' },
|
||||
{ title: 'ClientAddr', dataIndex: 'clientAddr' },
|
||||
{ title: 'Language', dataIndex: 'language' },
|
||||
{ title: 'Version', dataIndex: 'versionDesc' },
|
||||
];
|
||||
|
||||
const subscriptionColumns = [
|
||||
{ title: 'Topic', dataIndex: 'topic' },
|
||||
{ title: 'SubExpression', dataIndex: 'subString' },
|
||||
];
|
||||
|
||||
return (
|
||||
<Modal
|
||||
title={`[${group}]${t.CLIENT}`}
|
||||
visible={visible}
|
||||
onCancel={onCancel}
|
||||
footer={null}
|
||||
width={800}
|
||||
>
|
||||
<Spin spinning={loading}>
|
||||
{connectionData && (
|
||||
<>
|
||||
<Table
|
||||
columns={connectionColumns}
|
||||
dataSource={connectionData.connectionSet}
|
||||
rowKey="clientId"
|
||||
pagination={false}
|
||||
/>
|
||||
<h4>{t.SUBSCRIPTION}</h4>
|
||||
<Table
|
||||
columns={subscriptionColumns}
|
||||
dataSource={
|
||||
subscriptionData?.subscriptionTable
|
||||
? Object.entries(subscriptionData.subscriptionTable).map(([topic, detail]) => ({
|
||||
topic,
|
||||
...detail,
|
||||
}))
|
||||
: []
|
||||
}
|
||||
rowKey="topic"
|
||||
pagination={false}
|
||||
locale={{
|
||||
emptyText: loading ? <Spin size="small" /> : t.NO_DATA
|
||||
}}
|
||||
/>
|
||||
<p>ConsumeType: {connectionData.consumeType}</p>
|
||||
<p>MessageModel: {connectionData.messageModel}</p>
|
||||
</>
|
||||
)}
|
||||
</Spin>
|
||||
</Modal>
|
||||
);
|
||||
};
|
||||
|
||||
export default ClientInfoModal;
|
287
frontend-new/src/components/consumer/ConsumerConfigItem.jsx
Normal file
287
frontend-new/src/components/consumer/ConsumerConfigItem.jsx
Normal file
@@ -0,0 +1,287 @@
|
||||
/*
|
||||
* Licensed to the Apache Software Foundation (ASF) under one or more
|
||||
* contributor license agreements. See the NOTICE file distributed with
|
||||
* this work for additional information regarding copyright ownership.
|
||||
* The ASF licenses this file to You under the Apache License, Version 2.0
|
||||
* (the "License"); you may not use this file except in compliance with
|
||||
* the License. You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import React, { useEffect, useState } from 'react';
|
||||
import { Button, Descriptions, Form, Input, Select, Switch, message } from 'antd';
|
||||
import { remoteApi } from '../../api/remoteApi/remoteApi'; // 确保路径正确
|
||||
|
||||
const { Option } = Select;
|
||||
|
||||
const ConsumerConfigItem = ({ initialConfig, isAddConfig, group, brokerName, allBrokerList, allClusterNames,onCancel, onSuccess, t }) => {
|
||||
const [form] = Form.useForm();
|
||||
const [currentBrokerName, setCurrentBrokerName] = useState(brokerName);
|
||||
|
||||
useEffect(() => {
|
||||
if (initialConfig) {
|
||||
if (!isAddConfig && initialConfig.brokerNameList && initialConfig.brokerNameList.length > 0) {
|
||||
// 更新模式,设置当前BrokerName为第一个(如果只有一个的话,或者您有其他选择逻辑)
|
||||
setCurrentBrokerName(initialConfig.brokerNameList[0]);
|
||||
}
|
||||
|
||||
form.setFieldsValue({
|
||||
...initialConfig.subscriptionGroupConfig,
|
||||
groupName: isAddConfig ? undefined : initialConfig.subscriptionGroupConfig.groupName, // 添加模式下groupName可编辑
|
||||
brokerName: isAddConfig ? [] : initialConfig.brokerNameList, // 更新模式下显示已有的brokerName
|
||||
clusterName: isAddConfig ? [] : initialConfig.clusterNameList, // 更新模式下显示已有的clusterName
|
||||
});
|
||||
} else {
|
||||
// Reset form for add mode or when initialConfig is null (e.g., when the modal is closed)
|
||||
form.resetFields();
|
||||
form.setFieldsValue({
|
||||
groupName: undefined,
|
||||
autoCommit: true,
|
||||
enableAutoCommit: true,
|
||||
enableAutoOffsetReset: true,
|
||||
groupSysFlag: 0,
|
||||
consumeTimeoutMinute: 10,
|
||||
consumeEnable: true,
|
||||
consumeMessageOrderly: false,
|
||||
consumeBroadcastEnable: false,
|
||||
retryQueueNums: 1,
|
||||
retryMaxTimes: 16,
|
||||
brokerId: 0,
|
||||
whichBrokerWhenConsumeSlowly: 0,
|
||||
brokerName: [],
|
||||
clusterName: [],
|
||||
});
|
||||
setCurrentBrokerName(undefined); // 清空当前brokerName
|
||||
}
|
||||
}, [initialConfig, isAddConfig, form]);
|
||||
|
||||
const handleSubmit = async () => {
|
||||
try {
|
||||
const values = await form.validateFields();
|
||||
const numericValues = {
|
||||
retryQueueNums: Number(values.retryQueueNums),
|
||||
retryMaxTimes: Number(values.retryMaxTimes),
|
||||
brokerId: Number(values.brokerId),
|
||||
whichBrokerWhenConsumeSlowly: Number(values.whichBrokerWhenConsumeSlowly),
|
||||
};
|
||||
|
||||
// 确保brokerNameList是数组
|
||||
let finalBrokerNameList = Array.isArray(values.brokerName) ? values.brokerName : [values.brokerName];
|
||||
// 确保clusterNameList是数组
|
||||
let finalClusterNameList = Array.isArray(values.clusterName) ? values.clusterName : [values.clusterName];
|
||||
|
||||
const payload = {
|
||||
subscriptionGroupConfig: {
|
||||
...(initialConfig && initialConfig.subscriptionGroupConfig ? initialConfig.subscriptionGroupConfig : {}), // 保留旧的配置,除非被新值覆盖
|
||||
...values,
|
||||
...numericValues,
|
||||
groupName: isAddConfig ? values.groupName : group, // 添加模式使用表单中的groupName,更新模式使用传入的group
|
||||
},
|
||||
brokerNameList: finalBrokerNameList,
|
||||
clusterNameList: isAddConfig ? finalClusterNameList : null, // 更新模式保留原有clusterNameList
|
||||
};
|
||||
|
||||
const response = await remoteApi.createOrUpdateConsumer(payload);
|
||||
if (response.status === 0) {
|
||||
message.success(t.SUCCESS);
|
||||
onSuccess();
|
||||
} else {
|
||||
message.error(`${t.OPERATION_FAILED}: ${response.errMsg}`);
|
||||
console.error('Failed to create or update consumer:', response.errMsg);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Validation failed or API call error:', error);
|
||||
message.error(t.FORM_VALIDATION_FAILED);
|
||||
} finally {
|
||||
onCancel()
|
||||
}
|
||||
};
|
||||
|
||||
// Helper function to parse input value to number
|
||||
const parseNumber = (event) => {
|
||||
const value = event.target.value;
|
||||
return value === '' ? undefined : Number(value);
|
||||
};
|
||||
|
||||
// 如果是添加模式,并且用户还没有选择brokerName,或者没有clusterName可供选择,则不渲染表单
|
||||
if (isAddConfig && (!allBrokerList || allBrokerList.length === 0 || !allClusterNames || allClusterNames.length === 0)) {
|
||||
return <p>{t.NO_DATA}</p>;
|
||||
}
|
||||
|
||||
return (
|
||||
<div style={{border: '1px solid #e8e8e8', padding: 20, marginBottom: 20, borderRadius: 8}}>
|
||||
{/* 标题根据当前BrokerName或“添加新配置”显示 */}
|
||||
<h3>{isAddConfig ? t.ADD_CONSUMER : `${t.CONFIG_FOR_BROKER}: ${currentBrokerName || 'N/A'}`}</h3>
|
||||
{!isAddConfig && initialConfig && (
|
||||
<Descriptions bordered column={2} style={{marginBottom: 24}} size="small">
|
||||
<Descriptions.Item label={t.CLUSTER_NAME} span={2}>
|
||||
{initialConfig.clusterNameList?.join(', ') || 'N/A'}
|
||||
</Descriptions.Item>
|
||||
<Descriptions.Item label={t.RETRY_POLICY} span={2}>
|
||||
<pre style={{margin: 0, maxHeight: '100px', overflow: 'auto', fontSize: '12px'}}>
|
||||
{JSON.stringify(
|
||||
initialConfig.subscriptionGroupConfig.groupRetryPolicy,
|
||||
null,
|
||||
2
|
||||
) || 'N/A'}
|
||||
</pre>
|
||||
</Descriptions.Item>
|
||||
<Descriptions.Item label={t.CONSUME_TIMEOUT}>
|
||||
{`${initialConfig.subscriptionGroupConfig.consumeTimeoutMinute} ${t.MINUTES}` || 'N/A'}
|
||||
</Descriptions.Item>
|
||||
<Descriptions.Item label={t.SYSTEM_FLAG}>
|
||||
{initialConfig.subscriptionGroupConfig.groupSysFlag || 'N/A'}
|
||||
</Descriptions.Item>
|
||||
</Descriptions>
|
||||
)}
|
||||
|
||||
<Form form={form} layout="vertical">
|
||||
<Form.Item
|
||||
name="groupName"
|
||||
label={t.GROUP_NAME}
|
||||
rules={[{required: true, message: t.CANNOT_BE_EMPTY}]}
|
||||
>
|
||||
<Input disabled={!isAddConfig}/>
|
||||
</Form.Item>
|
||||
|
||||
{isAddConfig && (
|
||||
<Form.Item
|
||||
name="clusterName"
|
||||
label={t.CLUSTER_NAME}
|
||||
rules={[{required: true, message: t.PLEASE_SELECT_CLUSTER_NAME}]}
|
||||
>
|
||||
<Select
|
||||
mode="multiple"
|
||||
placeholder={t.SELECT_CLUSTERS}
|
||||
disabled={!isAddConfig}
|
||||
>
|
||||
{allClusterNames.map((cluster) => (
|
||||
<Option key={cluster} value={cluster}>
|
||||
{cluster}
|
||||
</Option>
|
||||
))}
|
||||
</Select>
|
||||
</Form.Item>
|
||||
)}
|
||||
|
||||
<Form.Item
|
||||
name="brokerName"
|
||||
label={t.BROKER_NAME}
|
||||
rules={[{required: true, message: t.PLEASE_SELECT_BROKER}]}
|
||||
>
|
||||
<Select
|
||||
mode="multiple"
|
||||
placeholder={t.SELECT_BROKERS}
|
||||
disabled={!isAddConfig} // 只有在添加模式下才能选择brokerName
|
||||
onChange={(selectedBrokers) => {
|
||||
if (isAddConfig && selectedBrokers.length > 0) {
|
||||
// 在添加模式下,如果选择了broker,则将第一个选中的broker设置为当前brokerName用于显示
|
||||
setCurrentBrokerName(selectedBrokers[0]);
|
||||
}
|
||||
}}
|
||||
>
|
||||
{allBrokerList.map((broker) => (
|
||||
<Option key={broker} value={broker}>
|
||||
{broker}
|
||||
</Option>
|
||||
))}
|
||||
</Select>
|
||||
</Form.Item>
|
||||
|
||||
|
||||
<Form.Item name="consumeEnable" label={t.CONSUME_ENABLE} valuePropName="checked">
|
||||
<Switch/>
|
||||
</Form.Item>
|
||||
|
||||
<Form.Item name="consumeMessageOrderly" label={t.ORDERLY_CONSUMPTION} valuePropName="checked">
|
||||
<Switch/>
|
||||
</Form.Item>
|
||||
|
||||
<Form.Item name="consumeBroadcastEnable" label={t.BROADCAST_CONSUMPTION} valuePropName="checked">
|
||||
<Switch/>
|
||||
</Form.Item>
|
||||
|
||||
<div style={{display: 'grid', gridTemplateColumns: '1fr 1fr', gap: 16}}>
|
||||
<Form.Item
|
||||
name="retryQueueNums"
|
||||
label={t.RETRY_QUEUES}
|
||||
rules={[{
|
||||
type: 'number',
|
||||
message: t.PLEASE_INPUT_NUMBER,
|
||||
transform: value => Number(value)
|
||||
}, {
|
||||
required: true,
|
||||
message: t.CANNOT_BE_EMPTY
|
||||
}]}
|
||||
getValueFromEvent={parseNumber}
|
||||
>
|
||||
<Input type="number"/>
|
||||
</Form.Item>
|
||||
|
||||
<Form.Item
|
||||
name="retryMaxTimes"
|
||||
label={t.MAX_RETRIES}
|
||||
rules={[{
|
||||
type: 'number',
|
||||
message: t.PLEASE_INPUT_NUMBER,
|
||||
transform: value => Number(value)
|
||||
}, {
|
||||
required: true,
|
||||
message: t.CANNOT_BE_EMPTY
|
||||
}]}
|
||||
getValueFromEvent={parseNumber}
|
||||
>
|
||||
<Input type="number"/>
|
||||
</Form.Item>
|
||||
|
||||
<Form.Item
|
||||
name="brokerId"
|
||||
label={t.BROKER_ID}
|
||||
rules={[{
|
||||
type: 'number',
|
||||
message: t.PLEASE_INPUT_NUMBER,
|
||||
transform: value => Number(value)
|
||||
}, {
|
||||
required: true,
|
||||
message: t.CANNOT_BE_EMPTY
|
||||
}]}
|
||||
getValueFromEvent={parseNumber}
|
||||
>
|
||||
<Input type="number"/>
|
||||
</Form.Item>
|
||||
|
||||
<Form.Item
|
||||
name="whichBrokerWhenConsumeSlowly"
|
||||
label={t.SLOW_CONSUMPTION_BROKER}
|
||||
rules={[{
|
||||
type: 'number',
|
||||
message: t.PLEASE_INPUT_NUMBER,
|
||||
transform: value => Number(value)
|
||||
}, {
|
||||
required: true,
|
||||
message: t.CANNOT_BE_EMPTY
|
||||
}]}
|
||||
getValueFromEvent={parseNumber}
|
||||
>
|
||||
<Input type="number"/>
|
||||
</Form.Item>
|
||||
</div>
|
||||
<div style={{textAlign: 'right', marginTop: 20}}>
|
||||
<Button type="primary" onClick={handleSubmit}>
|
||||
{t.COMMIT}
|
||||
</Button>
|
||||
</div>
|
||||
</Form>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default ConsumerConfigItem;
|
||||
|
169
frontend-new/src/components/consumer/ConsumerConfigModal.jsx
Normal file
169
frontend-new/src/components/consumer/ConsumerConfigModal.jsx
Normal file
@@ -0,0 +1,169 @@
|
||||
/*
|
||||
* Licensed to the Apache Software Foundation (ASF) under one or more
|
||||
* contributor license agreements. See the NOTICE file distributed with
|
||||
* this work for additional information regarding copyright ownership.
|
||||
* The ASF licenses this file to You under the Apache License, Version 2.0
|
||||
* (the "License"); you may not use this file except in compliance with
|
||||
* the License. You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import React, {useEffect, useState} from 'react';
|
||||
import {Button, Modal, Spin} from 'antd';
|
||||
import {remoteApi} from '../../api/remoteApi/remoteApi';
|
||||
import {useLanguage} from '../../i18n/LanguageContext';
|
||||
import ConsumerConfigItem from './ConsumerConfigItem'; // 导入子组件
|
||||
|
||||
const ConsumerConfigModal = ({visible, isAddConfig, group, onCancel, setIsAddConfig, onSuccess}) => {
|
||||
const {t} = useLanguage();
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [allBrokerList, setAllBrokerList] = useState([]); // 存储所有可用的broker
|
||||
const [allClusterNames, setAllClusterNames] = useState([]); // 存储所有可用的cluster names
|
||||
const [initialConfigData, setInitialConfigData] = useState({}); // 存储按brokerName分的初始配置数据
|
||||
|
||||
useEffect(() => {
|
||||
if (visible) {
|
||||
const fetchInitialData = async () => {
|
||||
setLoading(true);
|
||||
try {
|
||||
// Fetch cluster list for broker names and cluster names
|
||||
if(isAddConfig) {
|
||||
const clusterResponse = await remoteApi.getClusterList();
|
||||
if (clusterResponse.status === 0 && clusterResponse.data) {
|
||||
const clusterInfo = clusterResponse.data.clusterInfo;
|
||||
|
||||
const brokers = [];
|
||||
const clusterNames = Object.keys(clusterInfo?.clusterAddrTable || {});
|
||||
|
||||
clusterNames.forEach(clusterName => {
|
||||
const brokersInCluster = clusterInfo?.clusterAddrTable?.[clusterName] || [];
|
||||
brokers.push(...brokersInCluster);
|
||||
});
|
||||
|
||||
setAllBrokerList([...new Set(brokers)]); // 确保brokerName唯一
|
||||
setAllClusterNames(clusterNames);
|
||||
|
||||
} else {
|
||||
console.error('Failed to fetch cluster list:', clusterResponse.errMsg);
|
||||
}
|
||||
}
|
||||
if (!isAddConfig) {
|
||||
// Fetch existing consumer config for update mode
|
||||
const consumerConfigResponse = await remoteApi.queryConsumerConfig(group);
|
||||
if (consumerConfigResponse.status === 0 && consumerConfigResponse.data && consumerConfigResponse.data.length > 0) {
|
||||
const configMap = {};
|
||||
consumerConfigResponse.data.forEach(config => {
|
||||
// 假设每个brokerName有一个独立的配置项
|
||||
config.brokerNameList.forEach(brokerName => {
|
||||
configMap[brokerName] = {
|
||||
...config,
|
||||
// 确保brokerNameList和clusterNameList是数组形式,即使API返回单值
|
||||
brokerNameList: Array.isArray(config.brokerNameList) ? config.brokerNameList : [config.brokerNameList],
|
||||
clusterNameList: Array.isArray(config.clusterNameList) ? config.clusterNameList : [config.clusterNameList]
|
||||
};
|
||||
});
|
||||
});
|
||||
setInitialConfigData(configMap);
|
||||
} else {
|
||||
console.error(`Failed to fetch consumer config for group: ${group}`);
|
||||
onCancel(); // Close modal if config not found
|
||||
}
|
||||
} else {
|
||||
// For add mode, initialize with empty values and allow selecting any broker
|
||||
setInitialConfigData({
|
||||
// 当isAddConfig为true时,我们只提供一个空的配置模板,用户选择broker后会创建新的配置
|
||||
// 在这里,我们将设置一个空的初始配置,供用户选择broker来创建新配置
|
||||
newConfig: {
|
||||
groupName: undefined,
|
||||
subscriptionGroupConfig: {
|
||||
autoCommit: true,
|
||||
enableAutoCommit: true,
|
||||
enableAutoOffsetReset: true,
|
||||
groupSysFlag: 0,
|
||||
consumeTimeoutMinute: 10,
|
||||
consumeEnable: true,
|
||||
consumeMessageOrderly: false,
|
||||
consumeBroadcastEnable: false,
|
||||
retryQueueNums: 1,
|
||||
retryMaxTimes: 16,
|
||||
brokerId: 0,
|
||||
whichBrokerWhenConsumeSlowly: 0,
|
||||
},
|
||||
brokerNameList: [],
|
||||
clusterNameList: []
|
||||
}
|
||||
});
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error in fetching initial data:', error);
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
fetchInitialData();
|
||||
} else {
|
||||
// Reset state when modal is closed
|
||||
setInitialConfigData({});
|
||||
setAllBrokerList([]);
|
||||
setAllClusterNames([]);
|
||||
}
|
||||
}, [visible, isAddConfig, group, onCancel]);
|
||||
|
||||
const getBrokersToRender = () => {
|
||||
if (isAddConfig) {
|
||||
return ['newConfig'];
|
||||
} else {
|
||||
return Object.keys(initialConfigData);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
return (
|
||||
<Modal
|
||||
title={isAddConfig ? t.ADD_CONSUMER : `${t.CONFIG} - ${group}`}
|
||||
visible={visible}
|
||||
onCancel={() => {
|
||||
onCancel();
|
||||
setIsAddConfig(false); // 确保关闭时重置添加模式
|
||||
}}
|
||||
width={800}
|
||||
footer={[
|
||||
<Button key="cancel" onClick={() => {
|
||||
onCancel();
|
||||
setIsAddConfig(false);
|
||||
}}>
|
||||
{t.CLOSE}
|
||||
</Button>,
|
||||
]}
|
||||
style={{top: 20}} // 让弹窗靠上一点,方便内容滚动
|
||||
bodyStyle={{maxHeight: 'calc(100vh - 200px)', overflowY: 'auto'}} // 允许内容滚动
|
||||
>
|
||||
<Spin spinning={loading}>
|
||||
{getBrokersToRender().map(brokerOrKey => (
|
||||
<ConsumerConfigItem
|
||||
key={brokerOrKey} // 使用brokerName作为key
|
||||
initialConfig={initialConfigData[brokerOrKey]}
|
||||
isAddConfig={isAddConfig}
|
||||
group={group} // 传递当前group
|
||||
brokerName={isAddConfig ? undefined : brokerOrKey} // 添加模式下brokerName由用户选择,更新模式下是当前遍历的brokerName
|
||||
allBrokerList={allBrokerList}
|
||||
allClusterNames={allClusterNames}
|
||||
onSuccess={onSuccess}
|
||||
onCancel={onCancel}
|
||||
t={t} // 传递i18n函数
|
||||
/>
|
||||
))}
|
||||
</Spin>
|
||||
</Modal>
|
||||
);
|
||||
};
|
||||
|
||||
export default ConsumerConfigModal;
|
79
frontend-new/src/components/consumer/ConsumerDetailModal.jsx
Normal file
79
frontend-new/src/components/consumer/ConsumerDetailModal.jsx
Normal file
@@ -0,0 +1,79 @@
|
||||
/*
|
||||
* Licensed to the Apache Software Foundation (ASF) under one or more
|
||||
* contributor license agreements. See the NOTICE file distributed with
|
||||
* this work for additional information regarding copyright ownership.
|
||||
* The ASF licenses this file to You under the Apache License, Version 2.0
|
||||
* (the "License"); you may not use this file except in compliance with
|
||||
* the License. You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import { Modal, Table, Spin } from 'antd';
|
||||
import { remoteApi } from '../../api/remoteApi/remoteApi';
|
||||
import { useLanguage } from '../../i18n/LanguageContext';
|
||||
|
||||
const ConsumerDetailModal = ({ visible, group, address, onCancel }) => {
|
||||
const { t } = useLanguage();
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [details, setDetails] = useState([]);
|
||||
|
||||
useEffect(() => {
|
||||
const fetchData = async () => {
|
||||
if (!visible) return;
|
||||
|
||||
setLoading(true);
|
||||
try {
|
||||
const response = await remoteApi.queryTopicByConsumer(group, address);
|
||||
if (response.status === 0) {
|
||||
setDetails(response.data);
|
||||
}
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
fetchData();
|
||||
}, [visible, group, address]);
|
||||
|
||||
const queueColumns = [
|
||||
{ title: 'Broker', dataIndex: 'brokerName' },
|
||||
{ title: 'Queue', dataIndex: 'queueId' },
|
||||
{ title: 'BrokerOffset', dataIndex: 'brokerOffset' },
|
||||
{ title: 'ConsumerOffset', dataIndex: 'consumerOffset' },
|
||||
{ title: 'DiffTotal', dataIndex: 'diffTotal' },
|
||||
{ title: 'LastTimestamp', dataIndex: 'lastTimestamp' },
|
||||
];
|
||||
|
||||
return (
|
||||
<Modal
|
||||
title={`[${group}]${t.CONSUME_DETAIL}`}
|
||||
visible={visible}
|
||||
onCancel={onCancel}
|
||||
footer={null}
|
||||
width={1200}
|
||||
>
|
||||
<Spin spinning={loading}>
|
||||
{details.map((consumeDetail, index) => (
|
||||
<div key={index}>
|
||||
<Table
|
||||
columns={queueColumns}
|
||||
dataSource={consumeDetail.queueStatInfoList}
|
||||
rowKey="queueId"
|
||||
pagination={false}
|
||||
/>
|
||||
</div>
|
||||
))}
|
||||
</Spin>
|
||||
</Modal>
|
||||
);
|
||||
};
|
||||
|
||||
export default ConsumerDetailModal;
|
110
frontend-new/src/components/consumer/DeleteConsumerModal.jsx
Normal file
110
frontend-new/src/components/consumer/DeleteConsumerModal.jsx
Normal file
@@ -0,0 +1,110 @@
|
||||
/*
|
||||
* Licensed to the Apache Software Foundation (ASF) under one or more
|
||||
* contributor license agreements. See the NOTICE file distributed with
|
||||
* this work for additional information regarding copyright ownership.
|
||||
* The ASF licenses this file to You under the Apache License, Version 2.0
|
||||
* (the "License"); you may not use this file except in compliance with
|
||||
* the License. You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import { Modal, Spin, Checkbox, Button, notification } from 'antd';
|
||||
import { remoteApi } from '../../api/remoteApi/remoteApi';
|
||||
import { useLanguage } from '../../i18n/LanguageContext';
|
||||
|
||||
const DeleteConsumerModal = ({ visible, group, onCancel, onSuccess }) => {
|
||||
const { t } = useLanguage();
|
||||
const [brokerList, setBrokerList] = useState([]);
|
||||
const [selectedBrokers, setSelectedBrokers] = useState([]);
|
||||
const [loading, setLoading] = useState(false);
|
||||
|
||||
// 获取Broker列表
|
||||
useEffect(() => {
|
||||
const fetchBrokers = async () => {
|
||||
if (!visible) return;
|
||||
|
||||
setLoading(true);
|
||||
try {
|
||||
const response = await remoteApi.fetchBrokerNameList(group);
|
||||
if (response.status === 0) {
|
||||
setBrokerList(response.data);
|
||||
}
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
fetchBrokers();
|
||||
}, [visible, group]);
|
||||
|
||||
// 处理删除提交
|
||||
const handleDelete = async () => {
|
||||
if (selectedBrokers.length === 0) {
|
||||
notification.warning({ message: t.PLEASE_SELECT_BROKER });
|
||||
return;
|
||||
}
|
||||
|
||||
setLoading(true);
|
||||
try {
|
||||
const response = await remoteApi.deleteConsumerGroup(
|
||||
group,
|
||||
selectedBrokers
|
||||
);
|
||||
|
||||
if (response.status === 0) {
|
||||
notification.success({ message: t.DELETE_SUCCESS });
|
||||
onSuccess();
|
||||
onCancel();
|
||||
}
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<Modal
|
||||
title={`${t.DELETE_CONSUMER_GROUP} - ${group}`}
|
||||
visible={visible}
|
||||
onCancel={onCancel}
|
||||
footer={[
|
||||
<Button key="cancel" onClick={onCancel}>
|
||||
{t.CANCEL}
|
||||
</Button>,
|
||||
<Button
|
||||
key="delete"
|
||||
type="primary"
|
||||
danger
|
||||
loading={loading}
|
||||
onClick={handleDelete}
|
||||
>
|
||||
{t.CONFIRM_DELETE}
|
||||
</Button>
|
||||
]}
|
||||
>
|
||||
<Spin spinning={loading}>
|
||||
<div style={{ marginBottom: 16 }}>{t.SELECT_DELETE_BROKERS}:</div>
|
||||
<Checkbox.Group
|
||||
style={{ width: '100%' }}
|
||||
value={selectedBrokers}
|
||||
onChange={values => setSelectedBrokers(values)}
|
||||
>
|
||||
{brokerList.map(broker => (
|
||||
<div key={broker}>
|
||||
<Checkbox value={broker}>{broker}</Checkbox>
|
||||
</div>
|
||||
))}
|
||||
</Checkbox.Group>
|
||||
</Spin>
|
||||
</Modal>
|
||||
);
|
||||
};
|
||||
|
||||
export default DeleteConsumerModal;
|
@@ -0,0 +1,76 @@
|
||||
/*
|
||||
* Licensed to the Apache Software Foundation (ASF) under one or more
|
||||
* contributor license agreements. See the NOTICE file distributed with
|
||||
* this work for additional information regarding copyright ownership.
|
||||
* The ASF licenses this file to You under the Apache License, Version 2.0
|
||||
* (the "License"); you may not use this file except in compliance with
|
||||
* the License. You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import { Button, DatePicker, Form, Modal, Select } from "antd";
|
||||
import React, { useEffect, useState } from "react";
|
||||
|
||||
const ConsumerResetOffsetDialog = ({ visible, onClose, topic, allConsumerGroupList, handleResetOffset, t }) => {
|
||||
const [form] = Form.useForm();
|
||||
const [selectedConsumerGroup, setSelectedConsumerGroup] = useState([]);
|
||||
const [selectedTime, setSelectedTime] = useState(null);
|
||||
|
||||
useEffect(() => {
|
||||
if (!visible) {
|
||||
setSelectedConsumerGroup([]);
|
||||
setSelectedTime(null);
|
||||
form.resetFields();
|
||||
}
|
||||
}, [visible, form]);
|
||||
|
||||
const handleResetButtonClick = () => {
|
||||
handleResetOffset(selectedConsumerGroup, selectedTime ? selectedTime.valueOf() : null);
|
||||
};
|
||||
|
||||
return (
|
||||
<Modal
|
||||
title={`${topic} ${t.RESET_OFFSET}`}
|
||||
open={visible}
|
||||
onCancel={onClose}
|
||||
footer={[
|
||||
<Button key="reset" type="primary" onClick={handleResetButtonClick}>
|
||||
{t.RESET}
|
||||
</Button>,
|
||||
<Button key="close" onClick={onClose}>
|
||||
{t.CLOSE}
|
||||
</Button>,
|
||||
]}
|
||||
>
|
||||
<Form form={form} layout="horizontal" labelCol={{ span: 6 }} wrapperCol={{ span: 18 }}>
|
||||
<Form.Item label={t.SUBSCRIPTION_GROUP} required>
|
||||
<Select
|
||||
mode="multiple"
|
||||
placeholder={t.SELECT_CONSUMER_GROUP}
|
||||
value={selectedConsumerGroup}
|
||||
onChange={setSelectedConsumerGroup}
|
||||
options={allConsumerGroupList.map(group => ({ value: group, label: group }))}
|
||||
/>
|
||||
</Form.Item>
|
||||
<Form.Item label={t.TIME} required>
|
||||
<DatePicker
|
||||
showTime
|
||||
format="YYYY-MM-DD HH:mm:ss"
|
||||
value={selectedTime}
|
||||
onChange={setSelectedTime}
|
||||
style={{ width: '100%' }}
|
||||
/>
|
||||
</Form.Item>
|
||||
</Form>
|
||||
</Modal>
|
||||
);
|
||||
};
|
||||
|
||||
export default ConsumerResetOffsetDialog;
|
96
frontend-new/src/components/topic/ConsumerViewDialog.jsx
Normal file
96
frontend-new/src/components/topic/ConsumerViewDialog.jsx
Normal file
@@ -0,0 +1,96 @@
|
||||
/*
|
||||
* Licensed to the Apache Software Foundation (ASF) under one or more
|
||||
* contributor license agreements. See the NOTICE file distributed with
|
||||
* this work for additional information regarding copyright ownership.
|
||||
* The ASF licenses this file to You under the Apache License, Version 2.0
|
||||
* (the "License"); you may not use this file except in compliance with
|
||||
* the License. You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import moment from "moment/moment";
|
||||
import {Button, Modal, Table} from "antd";
|
||||
import React from "react";
|
||||
|
||||
const ConsumerViewDialog = ({ visible, onClose, topic, consumerData, consumerGroupCount, t }) => {
|
||||
const columns = [
|
||||
{ title: t.BROKER, dataIndex: 'brokerName', key: 'brokerName', align: 'center' },
|
||||
{ title: t.QUEUE, dataIndex: 'queueId', key: 'queueId', align: 'center' },
|
||||
{ title: t.CONSUMER_CLIENT, dataIndex: 'clientInfo', key: 'clientInfo', align: 'center' },
|
||||
{ title: t.BROKER_OFFSET, dataIndex: 'brokerOffset', key: 'brokerOffset', align: 'center' },
|
||||
{ title: t.CONSUMER_OFFSET, dataIndex: 'consumerOffset', key: 'consumerOffset', align: 'center' },
|
||||
{
|
||||
title: t.DIFF_TOTAL,
|
||||
dataIndex: 'diffTotal',
|
||||
key: 'diffTotal',
|
||||
align: 'center',
|
||||
render: (_, record) => record.brokerOffset - record.consumerOffset,
|
||||
},
|
||||
{
|
||||
title: t.LAST_TIME_STAMP,
|
||||
dataIndex: 'lastTimestamp',
|
||||
key: 'lastTimestamp',
|
||||
align: 'center',
|
||||
render: (text) => moment(text).format('YYYY-MM-DD HH:mm:ss'),
|
||||
},
|
||||
];
|
||||
|
||||
return (
|
||||
<Modal
|
||||
title={`${topic} ${t.SUBSCRIPTION_GROUP}`}
|
||||
open={visible}
|
||||
onCancel={onClose}
|
||||
width={1000}
|
||||
footer={[
|
||||
<Button key="close" onClick={onClose}>
|
||||
{t.CLOSE}
|
||||
</Button>,
|
||||
]}
|
||||
>
|
||||
{consumerGroupCount === 0 ? (
|
||||
<div>{t.NO_DATA} {t.SUBSCRIPTION_GROUP}</div>
|
||||
) : (
|
||||
consumerData && Object.entries(consumerData).map(([consumerGroup, consumeDetail]) => (
|
||||
<div key={consumerGroup} style={{ marginBottom: '24px' }}>
|
||||
<Table
|
||||
bordered
|
||||
pagination={false}
|
||||
showHeader={false}
|
||||
dataSource={[{ consumerGroup, diffTotal: consumeDetail.diffTotal, lastTimestamp: consumeDetail.lastTimestamp }]}
|
||||
columns={[
|
||||
{ title: t.SUBSCRIPTION_GROUP, dataIndex: 'consumerGroup', key: 'consumerGroup' },
|
||||
{ title: t.DELAY, dataIndex: 'diffTotal', key: 'diffTotal' },
|
||||
{
|
||||
title: t.LAST_CONSUME_TIME,
|
||||
dataIndex: 'lastTimestamp',
|
||||
key: 'lastTimestamp',
|
||||
render: (text) => moment(text).format('YYYY-MM-DD HH:mm:ss'),
|
||||
},
|
||||
]}
|
||||
rowKey="consumerGroup"
|
||||
size="small"
|
||||
style={{ marginBottom: '12px' }}
|
||||
/>
|
||||
<Table
|
||||
bordered
|
||||
pagination={false}
|
||||
dataSource={consumeDetail.queueStatInfoList}
|
||||
columns={columns}
|
||||
rowKey={(record, index) => `${record.brokerName}-${record.queueId}-${index}`}
|
||||
size="small"
|
||||
/>
|
||||
</div>
|
||||
))
|
||||
)}
|
||||
</Modal>
|
||||
);
|
||||
};
|
||||
|
||||
export default ConsumerViewDialog;
|
@@ -0,0 +1,65 @@
|
||||
/*
|
||||
* Licensed to the Apache Software Foundation (ASF) under one or more
|
||||
* contributor license agreements. See the NOTICE file distributed with
|
||||
* this work for additional information regarding copyright ownership.
|
||||
* The ASF licenses this file to You under the Apache License, Version 2.0
|
||||
* (the "License"); you may not use this file except in compliance with
|
||||
* the License. You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import {Button, Modal, Table} from "antd";
|
||||
import React from "react";
|
||||
|
||||
const ResetOffsetResultDialog = ({ visible, onClose, result, t }) => {
|
||||
return (
|
||||
<Modal
|
||||
title="ResetResult"
|
||||
open={visible}
|
||||
onCancel={onClose}
|
||||
footer={[
|
||||
<Button key="close" onClick={onClose}>
|
||||
{t.CLOSE}
|
||||
</Button>,
|
||||
]}
|
||||
>
|
||||
{result && Object.entries(result).map(([groupName, groupData]) => (
|
||||
<div key={groupName} style={{ marginBottom: '16px', border: '1px solid #f0f0f0', padding: '10px' }}>
|
||||
<Table
|
||||
dataSource={[{ groupName, status: groupData.status }]}
|
||||
columns={[
|
||||
{ title: 'GroupName', dataIndex: 'groupName', key: 'groupName' },
|
||||
{ title: 'State', dataIndex: 'status', key: 'status' },
|
||||
]}
|
||||
pagination={false}
|
||||
rowKey="groupName"
|
||||
size="small"
|
||||
bordered
|
||||
/>
|
||||
{groupData.rollbackStatsList === null ? (
|
||||
<div>You Should Check It Yourself</div>
|
||||
) : (
|
||||
<Table
|
||||
dataSource={groupData.rollbackStatsList.map((item, index) => ({ key: index, item }))}
|
||||
columns={[{ dataIndex: 'item', key: 'item' }]}
|
||||
pagination={false}
|
||||
rowKey="key"
|
||||
size="small"
|
||||
bordered
|
||||
showHeader={false}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
))}
|
||||
</Modal>
|
||||
);
|
||||
};
|
||||
|
||||
export default ResetOffsetResultDialog;
|
111
frontend-new/src/components/topic/RouterViewDialog.jsx
Normal file
111
frontend-new/src/components/topic/RouterViewDialog.jsx
Normal file
@@ -0,0 +1,111 @@
|
||||
/*
|
||||
* Licensed to the Apache Software Foundation (ASF) under one or more
|
||||
* contributor license agreements. See the NOTICE file distributed with
|
||||
* this work for additional information regarding copyright ownership.
|
||||
* The ASF licenses this file to You under the Apache License, Version 2.0
|
||||
* (the "License"); you may not use this file except in compliance with
|
||||
* the License. You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import { Button, Modal, Table } from "antd";
|
||||
import React from "react";
|
||||
|
||||
const RouterViewDialog = ({ visible, onClose, topic, routeData, t }) => {
|
||||
const brokerColumns = [
|
||||
{
|
||||
title: 'Broker',
|
||||
dataIndex: 'brokerName',
|
||||
key: 'brokerName',
|
||||
},
|
||||
{
|
||||
title: 'Broker Addrs',
|
||||
key: 'brokerAddrs',
|
||||
render: (_, record) => (
|
||||
<Table
|
||||
dataSource={Object.entries(record.brokerAddrs || []).map(([key, value]) => ({ key, idx: key, address: value }))}
|
||||
columns={[
|
||||
{ title: 'Index', dataIndex: 'idx', key: 'idx' },
|
||||
{ title: 'Address', dataIndex: 'address', key: 'address' },
|
||||
]}
|
||||
pagination={false}
|
||||
bordered
|
||||
size="small"
|
||||
/>
|
||||
),
|
||||
},
|
||||
];
|
||||
|
||||
const queueColumns = [
|
||||
{
|
||||
title: t.BROKER_NAME,
|
||||
dataIndex: 'brokerName',
|
||||
key: 'brokerName',
|
||||
},
|
||||
{
|
||||
title: t.READ_QUEUE_NUMS,
|
||||
dataIndex: 'readQueueNums',
|
||||
key: 'readQueueNums',
|
||||
},
|
||||
{
|
||||
title: t.WRITE_QUEUE_NUMS,
|
||||
dataIndex: 'writeQueueNums',
|
||||
key: 'writeQueueNums',
|
||||
},
|
||||
{
|
||||
title: t.PERM,
|
||||
dataIndex: 'perm',
|
||||
key: 'perm',
|
||||
},
|
||||
];
|
||||
|
||||
return (
|
||||
<Modal
|
||||
title={`${topic}${t.ROUTER}`}
|
||||
open={visible}
|
||||
onCancel={onClose}
|
||||
width={800}
|
||||
footer={[
|
||||
<Button key="close" onClick={onClose}>
|
||||
{t.CLOSE}
|
||||
</Button>,
|
||||
]}
|
||||
>
|
||||
<div className="limit_height">
|
||||
<div>
|
||||
<h3>Broker Datas:</h3>
|
||||
{routeData?.brokerDatas?.map((item, index) => (
|
||||
<div key={index} style={{ marginBottom: '15px', border: '1px solid #d9d9d9', padding: '10px' }}>
|
||||
<Table
|
||||
dataSource={[item]}
|
||||
columns={brokerColumns}
|
||||
pagination={false}
|
||||
bordered
|
||||
size="small"
|
||||
/>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
<div style={{ marginTop: '20px' }}>
|
||||
<h3>{t.QUEUE_DATAS}:</h3>
|
||||
<Table
|
||||
dataSource={routeData?.queueDatas || []}
|
||||
columns={queueColumns}
|
||||
pagination={false}
|
||||
bordered
|
||||
size="small"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</Modal>
|
||||
);
|
||||
};
|
||||
|
||||
export default RouterViewDialog;
|
65
frontend-new/src/components/topic/SendResultDialog.jsx
Normal file
65
frontend-new/src/components/topic/SendResultDialog.jsx
Normal file
@@ -0,0 +1,65 @@
|
||||
/*
|
||||
* Licensed to the Apache Software Foundation (ASF) under one or more
|
||||
* contributor license agreements. See the NOTICE file distributed with
|
||||
* this work for additional information regarding copyright ownership.
|
||||
* The ASF licenses this file to You under the Apache License, Version 2.0
|
||||
* (the "License"); you may not use this file except in compliance with
|
||||
* the License. You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import {Button, Form, Modal, Table} from "antd";
|
||||
import React from "react";
|
||||
|
||||
const SendResultDialog = ({ visible, onClose, result, t }) => {
|
||||
return (
|
||||
<Modal
|
||||
title="SendResult"
|
||||
open={visible}
|
||||
onCancel={onClose}
|
||||
footer={[
|
||||
<Button key="close" onClick={onClose}>
|
||||
{t.CLOSE}
|
||||
</Button>,
|
||||
]}
|
||||
>
|
||||
<Form layout="horizontal">
|
||||
<Table
|
||||
bordered
|
||||
dataSource={
|
||||
result
|
||||
? Object.entries(result).map(([key, value], index) => ({
|
||||
key: index,
|
||||
label: key,
|
||||
value: typeof value === 'object' ? JSON.stringify(value, null, 2) : String(value),
|
||||
}))
|
||||
: []
|
||||
}
|
||||
columns={[
|
||||
{ dataIndex: 'label', key: 'label' },
|
||||
{
|
||||
dataIndex: 'value',
|
||||
key: 'value',
|
||||
render: (text) => <pre style={{ whiteSpace: 'pre-wrap', margin: 0 }}>{text}</pre>,
|
||||
},
|
||||
]}
|
||||
pagination={false}
|
||||
showHeader={false}
|
||||
rowKey="key"
|
||||
size="small"
|
||||
/>
|
||||
</Form>
|
||||
</Modal>
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
|
||||
export default SendResultDialog;
|
102
frontend-new/src/components/topic/SendTopicMessageDialog.jsx
Normal file
102
frontend-new/src/components/topic/SendTopicMessageDialog.jsx
Normal file
@@ -0,0 +1,102 @@
|
||||
/*
|
||||
* Licensed to the Apache Software Foundation (ASF) under one or more
|
||||
* contributor license agreements. See the NOTICE file distributed with
|
||||
* this work for additional information regarding copyright ownership.
|
||||
* The ASF licenses this file to You under the Apache License, Version 2.0
|
||||
* (the "License"); you may not use this file except in compliance with
|
||||
* the License. You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import {Button, Checkbox, Form, Input, message, Modal} from "antd";
|
||||
import React, {useEffect} from "react";
|
||||
import {remoteApi} from "../../api/remoteApi/remoteApi";
|
||||
|
||||
const SendTopicMessageDialog = ({
|
||||
visible,
|
||||
onClose,
|
||||
topic,
|
||||
setSendResultData,
|
||||
setIsSendResultModalVisible,
|
||||
setIsSendTopicMessageModalVisible,
|
||||
t,
|
||||
}) => {
|
||||
const [form] = Form.useForm();
|
||||
|
||||
useEffect(() => {
|
||||
if (visible) {
|
||||
form.setFieldsValue({
|
||||
topic: topic,
|
||||
tag: '',
|
||||
key: '',
|
||||
messageBody: '',
|
||||
traceEnabled: false,
|
||||
});
|
||||
} else {
|
||||
form.resetFields();
|
||||
}
|
||||
}, [visible, topic, form]);
|
||||
|
||||
const handleSendTopicMessage = async () => {
|
||||
try {
|
||||
const values = await form.validateFields(); // 👈 从表单获取最新值
|
||||
const result = await remoteApi.sendTopicMessage(values); // 👈 用表单数据发送
|
||||
if (result.status === 0) {
|
||||
setSendResultData(result.data);
|
||||
setIsSendResultModalVisible(true);
|
||||
setIsSendTopicMessageModalVisible(false);
|
||||
} else {
|
||||
message.error(result.errMsg);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("Error sending message:", error);
|
||||
message.error("Failed to send message");
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<Modal
|
||||
title={`${t.SEND}[${topic}]${t.MESSAGE}`}
|
||||
open={visible}
|
||||
onCancel={onClose}
|
||||
footer={[
|
||||
<Button key="commit" type="primary" onClick={handleSendTopicMessage}>
|
||||
{t.COMMIT}
|
||||
</Button>,
|
||||
<Button key="close" onClick={onClose}>
|
||||
{t.CLOSE}
|
||||
</Button>,
|
||||
]}
|
||||
>
|
||||
<Form form={form} layout="horizontal" labelCol={{ span: 6 }} wrapperCol={{ span: 18 }}>
|
||||
<Form.Item label={t.TOPIC} name="topic">
|
||||
<Input disabled />
|
||||
</Form.Item>
|
||||
<Form.Item label={t.TAG} name="tag">
|
||||
<Input />
|
||||
</Form.Item>
|
||||
<Form.Item label={t.KEY} name="key">
|
||||
<Input />
|
||||
</Form.Item>
|
||||
<Form.Item label={t.MESSAGE_BODY} name="messageBody" rules={[{ required: true, message: t.REQUIRED }]}>
|
||||
<Input.TextArea
|
||||
style={{ maxHeight: '200px', minHeight: '200px', resize: 'none' }}
|
||||
rows={8}
|
||||
/>
|
||||
</Form.Item>
|
||||
<Form.Item label={t.ENABLE_MESSAGE_TRACE} name="traceEnabled" valuePropName="checked">
|
||||
<Checkbox />
|
||||
</Form.Item>
|
||||
</Form>
|
||||
</Modal>
|
||||
);
|
||||
};
|
||||
|
||||
export default SendTopicMessageDialog;
|
@@ -0,0 +1,70 @@
|
||||
/*
|
||||
* Licensed to the Apache Software Foundation (ASF) under one or more
|
||||
* contributor license agreements. See the NOTICE file distributed with
|
||||
* this work for additional information regarding copyright ownership.
|
||||
* The ASF licenses this file to You under the Apache License, Version 2.0
|
||||
* (the "License"); you may not use this file except in compliance with
|
||||
* the License. You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import { Button, Form, message, Modal, Select } from "antd";
|
||||
import React, { useEffect, useState } from "react";
|
||||
|
||||
const SkipMessageAccumulateDialog = ({ visible, onClose, topic, allConsumerGroupList, handleSkipMessageAccumulate, t }) => {
|
||||
const [form] = Form.useForm();
|
||||
const [selectedConsumerGroup, setSelectedConsumerGroup] = useState([]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!visible) {
|
||||
setSelectedConsumerGroup([]);
|
||||
form.resetFields();
|
||||
}
|
||||
}, [visible, form]);
|
||||
|
||||
const handleCommit = () => {
|
||||
if (!selectedConsumerGroup.length) {
|
||||
message.error(t.PLEASE_SELECT_GROUP);
|
||||
return;
|
||||
}
|
||||
handleSkipMessageAccumulate(selectedConsumerGroup);
|
||||
onClose();
|
||||
};
|
||||
|
||||
return (
|
||||
<Modal
|
||||
title={`${topic} ${t.SKIP_MESSAGE_ACCUMULATE}`}
|
||||
open={visible}
|
||||
onCancel={onClose}
|
||||
footer={[
|
||||
<Button key="commit" type="primary" onClick={handleCommit}>
|
||||
{t.COMMIT}
|
||||
</Button>,
|
||||
<Button key="close" onClick={onClose}>
|
||||
{t.CLOSE}
|
||||
</Button>,
|
||||
]}
|
||||
>
|
||||
<Form form={form} layout="horizontal" labelCol={{ span: 6 }} wrapperCol={{ span: 18 }}>
|
||||
<Form.Item label={t.SUBSCRIPTION_GROUP} required>
|
||||
<Select
|
||||
mode="multiple"
|
||||
placeholder={t.SELECT_CONSUMER_GROUP}
|
||||
value={selectedConsumerGroup}
|
||||
onChange={setSelectedConsumerGroup}
|
||||
options={allConsumerGroupList.map(group => ({ value: group, label: group }))}
|
||||
/>
|
||||
</Form.Item>
|
||||
</Form>
|
||||
</Modal>
|
||||
);
|
||||
};
|
||||
|
||||
export default SkipMessageAccumulateDialog;
|
66
frontend-new/src/components/topic/StatsViewDialog.jsx
Normal file
66
frontend-new/src/components/topic/StatsViewDialog.jsx
Normal file
@@ -0,0 +1,66 @@
|
||||
/*
|
||||
* Licensed to the Apache Software Foundation (ASF) under one or more
|
||||
* contributor license agreements. See the NOTICE file distributed with
|
||||
* this work for additional information regarding copyright ownership.
|
||||
* The ASF licenses this file to You under the Apache License, Version 2.0
|
||||
* (the "License"); you may not use this file except in compliance with
|
||||
* the License. You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import moment from "moment/moment";
|
||||
import {Button, Modal, Table} from "antd";
|
||||
import React from "react";
|
||||
|
||||
const StatsViewDialog = ({ visible, onClose, topic, statsData, t }) => {
|
||||
const columns = [
|
||||
{ title: t.QUEUE, dataIndex: 'queue', key: 'queue', align: 'center' },
|
||||
{ title: t.MIN_OFFSET, dataIndex: 'minOffset', key: 'minOffset', align: 'center' },
|
||||
{ title: t.MAX_OFFSET, dataIndex: 'maxOffset', key: 'maxOffset', align: 'center' },
|
||||
{
|
||||
title: t.LAST_UPDATE_TIME_STAMP,
|
||||
dataIndex: 'lastUpdateTimestamp',
|
||||
key: 'lastUpdateTimestamp',
|
||||
align: 'center',
|
||||
render: (text) => moment(text).format('YYYY-MM-DD HH:mm:ss'),
|
||||
},
|
||||
];
|
||||
|
||||
const dataSource = statsData?.offsetTable ? Object.entries(statsData.offsetTable).map(([queue, info]) => ({
|
||||
key: queue,
|
||||
queue: queue,
|
||||
...info,
|
||||
})) : [];
|
||||
|
||||
return (
|
||||
<Modal
|
||||
title={`[${topic}]${t.STATUS}`}
|
||||
open={visible}
|
||||
onCancel={onClose}
|
||||
width={800}
|
||||
footer={[
|
||||
<Button key="close" onClick={onClose}>
|
||||
{t.CLOSE}
|
||||
</Button>,
|
||||
]}
|
||||
>
|
||||
<Table
|
||||
bordered
|
||||
dataSource={dataSource}
|
||||
columns={columns}
|
||||
pagination={false}
|
||||
rowKey="key"
|
||||
size="small"
|
||||
/>
|
||||
</Modal>
|
||||
);
|
||||
};
|
||||
|
||||
export default StatsViewDialog;
|
65
frontend-new/src/components/topic/TopicModifyDialog.jsx
Normal file
65
frontend-new/src/components/topic/TopicModifyDialog.jsx
Normal file
@@ -0,0 +1,65 @@
|
||||
/*
|
||||
* Licensed to the Apache Software Foundation (ASF) under one or more
|
||||
* contributor license agreements. See the NOTICE file distributed with
|
||||
* this work for additional information regarding copyright ownership.
|
||||
* The ASF licenses this file to You under the Apache License, Version 2.0
|
||||
* (the "License"); you may not use this file except in compliance with
|
||||
* the License. You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
// TopicModifyDialog.js
|
||||
import { Button, Modal } from "antd";
|
||||
import React from "react";
|
||||
import TopicSingleModifyForm from './TopicSingleModifyForm';
|
||||
|
||||
const TopicModifyDialog = ({
|
||||
visible,
|
||||
onClose,
|
||||
initialData,
|
||||
bIsUpdate,
|
||||
writeOperationEnabled,
|
||||
allClusterNameList,
|
||||
allBrokerNameList,
|
||||
onSubmit,
|
||||
t,
|
||||
}) => {
|
||||
|
||||
return (
|
||||
<Modal
|
||||
title={bIsUpdate ? t.TOPIC_CHANGE : t.TOPIC_ADD}
|
||||
open={visible}
|
||||
onCancel={onClose}
|
||||
width={700}
|
||||
footer={[
|
||||
<Button key="close" onClick={onClose}>
|
||||
{t.CLOSE}
|
||||
</Button>,
|
||||
]}
|
||||
Style={{ maxHeight: '70vh', overflowY: 'auto' }}
|
||||
>
|
||||
{initialData.map((data, index) => (
|
||||
<TopicSingleModifyForm
|
||||
key={index}
|
||||
initialData={data}
|
||||
bIsUpdate={bIsUpdate}
|
||||
writeOperationEnabled={writeOperationEnabled}
|
||||
allClusterNameList={allClusterNameList}
|
||||
allBrokerNameList={allBrokerNameList}
|
||||
onSubmit={onSubmit}
|
||||
formIndex={index}
|
||||
t={t}
|
||||
/>
|
||||
))}
|
||||
</Modal>
|
||||
);
|
||||
};
|
||||
|
||||
export default TopicModifyDialog;
|
144
frontend-new/src/components/topic/TopicSingleModifyForm.jsx
Normal file
144
frontend-new/src/components/topic/TopicSingleModifyForm.jsx
Normal file
@@ -0,0 +1,144 @@
|
||||
/*
|
||||
* Licensed to the Apache Software Foundation (ASF) under one or more
|
||||
* contributor license agreements. See the NOTICE file distributed with
|
||||
* this work for additional information regarding copyright ownership.
|
||||
* The ASF licenses this file to You under the Apache License, Version 2.0
|
||||
* (the "License"); you may not use this file except in compliance with
|
||||
* the License. You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
// TopicSingleModifyForm.js
|
||||
import React, { useEffect } from "react";
|
||||
import {Button, Form, Input, Select, Divider, Row, Col} from "antd";
|
||||
|
||||
const TopicSingleModifyForm = ({
|
||||
initialData,
|
||||
bIsUpdate,
|
||||
writeOperationEnabled,
|
||||
allClusterNameList,
|
||||
allBrokerNameList,
|
||||
onSubmit,
|
||||
formIndex,
|
||||
t,
|
||||
}) => {
|
||||
const [form] = Form.useForm();
|
||||
|
||||
useEffect(() => {
|
||||
if (initialData) {
|
||||
form.setFieldsValue(initialData);
|
||||
} else {
|
||||
form.resetFields();
|
||||
}
|
||||
}, [initialData, form, formIndex]);
|
||||
|
||||
const handleFormSubmit = () => {
|
||||
form.validateFields()
|
||||
.then(values => {
|
||||
const updatedValues = { ...values };
|
||||
// 提交时,如果 clusterNameList 或 brokerNameList 为空,则填充所有可用的名称
|
||||
if(!bIsUpdate){
|
||||
if (!updatedValues.clusterNameList || updatedValues.clusterNameList.length === 0) {
|
||||
updatedValues.clusterNameList = allClusterNameList;
|
||||
}
|
||||
if (!updatedValues.brokerNameList || updatedValues.brokerNameList.length === 0) {
|
||||
updatedValues.brokerNameList = allBrokerNameList;
|
||||
}
|
||||
}
|
||||
onSubmit(updatedValues, formIndex); // 传递 formIndex
|
||||
})
|
||||
.catch(info => {
|
||||
console.log('Validate Failed:', info);
|
||||
});
|
||||
};
|
||||
|
||||
const messageTypeOptions = [
|
||||
{ value: 'TRANSACTION', label: 'TRANSACTION' },
|
||||
{ value: 'FIFO', label: 'FIFO' },
|
||||
{ value: 'DELAY', label: 'DELAY' },
|
||||
{ value: 'NORMAL', label: 'NORMAL' },
|
||||
];
|
||||
|
||||
return (
|
||||
<div style={{ paddingBottom: 24 }}>
|
||||
{bIsUpdate && <Divider orientation="left">{`${t.TOPIC_CONFIG} - ${initialData.brokerNameList ? initialData.brokerNameList.join(', ') : t.UNKNOWN_BROKER}`}</Divider>}
|
||||
<Row justify="center"> {/* 使用 Row 居中内容 */}
|
||||
<Col span={16}> {/* 表单内容占据 12 栅格宽度,并自动居中 */}
|
||||
<Form
|
||||
form={form}
|
||||
layout="horizontal"
|
||||
labelCol={{ span: 8 }}
|
||||
wrapperCol={{ span: 16 }}
|
||||
>
|
||||
<Form.Item label={t.CLUSTER_NAME} name="clusterNameList">
|
||||
<Select
|
||||
mode="multiple"
|
||||
disabled={bIsUpdate}
|
||||
placeholder={t.SELECT_CLUSTER_NAME}
|
||||
options={allClusterNameList.map(name => ({ value: name, label: name }))}
|
||||
/>
|
||||
</Form.Item>
|
||||
<Form.Item label="BROKER_NAME" name="brokerNameList">
|
||||
<Select
|
||||
mode="multiple"
|
||||
disabled={bIsUpdate}
|
||||
placeholder={t.SELECT_BROKER_NAME}
|
||||
options={allBrokerNameList.map(name => ({ value: name, label: name }))}
|
||||
/>
|
||||
</Form.Item>
|
||||
<Form.Item
|
||||
label={t.TOPIC_NAME}
|
||||
name="topicName"
|
||||
rules={[{ required: true, message: `${t.TOPIC_NAME}${t.CANNOT_BE_EMPTY}` }]}
|
||||
>
|
||||
<Input disabled={bIsUpdate} />
|
||||
</Form.Item>
|
||||
<Form.Item label={t.MESSAGE_TYPE} name="messageType">
|
||||
<Select
|
||||
disabled={bIsUpdate}
|
||||
options={messageTypeOptions}
|
||||
/>
|
||||
</Form.Item>
|
||||
<Form.Item
|
||||
label={t.WRITE_QUEUE_NUMS}
|
||||
name="writeQueueNums"
|
||||
rules={[{ required: true, message: `${t.WRITE_QUEUE_NUMS}${t.CANNOT_BE_EMPTY}` }]}
|
||||
>
|
||||
<Input disabled={!writeOperationEnabled} />
|
||||
</Form.Item>
|
||||
<Form.Item
|
||||
label={t.READ_QUEUE_NUMS}
|
||||
name="readQueueNums"
|
||||
rules={[{ required: true, message: `${t.READ_QUEUE_NUMS}${t.CANNOT_BE_EMPTY}` }]}
|
||||
>
|
||||
<Input disabled={!writeOperationEnabled} />
|
||||
</Form.Item>
|
||||
<Form.Item
|
||||
label={t.PERM}
|
||||
name="perm"
|
||||
rules={[{ required: true, message: `${t.PERM}${t.CANNOT_BE_EMPTY}` }]}
|
||||
>
|
||||
<Input disabled={!writeOperationEnabled} />
|
||||
</Form.Item>
|
||||
{!initialData.sysFlag && writeOperationEnabled && (
|
||||
<Form.Item wrapperCol={{ offset: 8, span: 16 }}>
|
||||
<Button type="primary" onClick={handleFormSubmit}>
|
||||
{t.COMMIT}
|
||||
</Button>
|
||||
</Form.Item>
|
||||
)}
|
||||
</Form>
|
||||
</Col>
|
||||
</Row>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default TopicSingleModifyForm;
|
143
frontend-new/src/pages/Producer/producer.jsx
Normal file
143
frontend-new/src/pages/Producer/producer.jsx
Normal file
@@ -0,0 +1,143 @@
|
||||
/*
|
||||
* Licensed to the Apache Software Foundation (ASF) under one or more
|
||||
* contributor license agreements. See the NOTICE file distributed with
|
||||
* this work for additional information regarding copyright ownership.
|
||||
* The ASF licenses this file to You under the Apache License, Version 2.0
|
||||
* (the "License"); you may not use this file except in compliance with
|
||||
* the License. You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import React, {useEffect, useState} from 'react';
|
||||
import {Button, Form, Input, message, Select, Table} from 'antd';
|
||||
import {remoteApi} from '../../api/remoteApi/remoteApi';
|
||||
|
||||
const {Option} = Select;
|
||||
|
||||
const ProducerConnectionList = () => {
|
||||
const [form] = Form.useForm();
|
||||
const [allTopicList, setAllTopicList] = useState([]);
|
||||
const [connectionList, setConnectionList] = useState([]);
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [messageApi, msgContextHolder] = message.useMessage();
|
||||
useEffect(() => {
|
||||
const fetchTopicList = async () => {
|
||||
setLoading(true);
|
||||
try {
|
||||
const resp = await remoteApi.queryTopic(true);
|
||||
if (!resp) {
|
||||
messageApi.error("Failed to fetch topic list - no response");
|
||||
return;
|
||||
}
|
||||
if (resp.status === 0) {
|
||||
setAllTopicList(resp.data.topicList.sort());
|
||||
} else {
|
||||
messageApi.error(resp.errMsg || "Failed to fetch topic list");
|
||||
}
|
||||
} catch (error) {
|
||||
messageApi.error("An error occurred while fetching topic list");
|
||||
console.error("Fetch error:", error);
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
fetchTopicList();
|
||||
}, []);
|
||||
|
||||
const onFinish = (values) => {
|
||||
setLoading(true);
|
||||
const {selectedTopic, producerGroup} = values;
|
||||
remoteApi.queryProducerConnection(selectedTopic, producerGroup, (resp) => {
|
||||
if (resp.status === 0) {
|
||||
setConnectionList(resp.data.connectionSet);
|
||||
} else {
|
||||
messageApi.error(resp.errMsg || "Failed to fetch producer connection list");
|
||||
}
|
||||
setLoading(false);
|
||||
});
|
||||
};
|
||||
|
||||
const columns = [
|
||||
{
|
||||
title: 'clientId',
|
||||
dataIndex: 'clientId',
|
||||
key: 'clientId',
|
||||
align: 'center',
|
||||
},
|
||||
{
|
||||
title: 'clientAddr',
|
||||
dataIndex: 'clientAddr',
|
||||
key: 'clientAddr',
|
||||
align: 'center',
|
||||
},
|
||||
{
|
||||
title: 'language',
|
||||
dataIndex: 'language',
|
||||
key: 'language',
|
||||
align: 'center',
|
||||
},
|
||||
{
|
||||
title: 'version',
|
||||
dataIndex: 'versionDesc',
|
||||
key: 'versionDesc',
|
||||
align: 'center',
|
||||
},
|
||||
];
|
||||
|
||||
return (
|
||||
<>
|
||||
{msgContextHolder}
|
||||
<div className="container-fluid" id="deployHistoryList">
|
||||
<Form
|
||||
form={form}
|
||||
layout="inline"
|
||||
onFinish={onFinish}
|
||||
style={{marginBottom: 20}}
|
||||
>
|
||||
<Form.Item label="TOPIC" name="selectedTopic"
|
||||
rules={[{required: true, message: 'Please select a topic!'}]}>
|
||||
<Select
|
||||
showSearch
|
||||
placeholder="Select a topic"
|
||||
style={{width: 300}}
|
||||
optionFilterProp="children"
|
||||
filterOption={(input, option) =>
|
||||
option.children.toLowerCase().indexOf(input.toLowerCase()) >= 0
|
||||
}
|
||||
>
|
||||
{allTopicList.map((topic) => (
|
||||
<Option key={topic} value={topic}>{topic}</Option>
|
||||
))}
|
||||
</Select>
|
||||
</Form.Item>
|
||||
<Form.Item label="PRODUCER_GROUP" name="producerGroup"
|
||||
rules={[{required: true, message: 'Please input producer group!'}]}>
|
||||
<Input style={{width: 300}}/>
|
||||
</Form.Item>
|
||||
<Form.Item>
|
||||
<Button type="primary" htmlType="submit" loading={loading}>
|
||||
<span className="glyphicon glyphicon-search"></span> SEARCH
|
||||
</Button>
|
||||
</Form.Item>
|
||||
</Form>
|
||||
<Table
|
||||
dataSource={connectionList}
|
||||
columns={columns}
|
||||
rowKey="clientId"
|
||||
pagination={false}
|
||||
bordered
|
||||
/>
|
||||
</div>
|
||||
</>
|
||||
|
||||
);
|
||||
};
|
||||
|
||||
export default ProducerConnectionList;
|
181
frontend-new/src/pages/Proxy/proxy.jsx
Normal file
181
frontend-new/src/pages/Proxy/proxy.jsx
Normal file
@@ -0,0 +1,181 @@
|
||||
/*
|
||||
* Licensed to the Apache Software Foundation (ASF) under one or more
|
||||
* contributor license agreements. See the NOTICE file distributed with
|
||||
* this work for additional information regarding copyright ownership.
|
||||
* The ASF licenses this file to You under the Apache License, Version 2.0
|
||||
* (the "License"); you may not use this file except in compliance with
|
||||
* the License. You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import { Modal, Button, Select, Input, Card, Row, Col, notification, Spin } from 'antd';
|
||||
import { useLanguage } from '../../i18n/LanguageContext';
|
||||
import { remoteApi } from "../../api/remoteApi/remoteApi";
|
||||
|
||||
|
||||
const { Option } = Select;
|
||||
|
||||
const ProxyManager = () => {
|
||||
const { t } = useLanguage();
|
||||
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [proxyAddrList, setProxyAddrList] = useState([]);
|
||||
const [selectedProxy, setSelectedProxy] = useState('');
|
||||
const [newProxyAddr, setNewProxyAddr] = useState('');
|
||||
const [allProxyConfig, setAllProxyConfig] = useState({});
|
||||
|
||||
const [showModal, setShowModal] = useState(false); // 控制 Modal 弹窗显示
|
||||
const [writeOperationEnabled, setWriteOperationEnabled] = useState(true); // 写操作权限,默认 true
|
||||
const [notificationApi, notificationContextHolder] = notification.useNotification();
|
||||
|
||||
useEffect(() => {
|
||||
const userRole = sessionStorage.getItem("userrole");
|
||||
const isWriteEnabled = userRole === null || userRole === '1';
|
||||
setWriteOperationEnabled(isWriteEnabled);
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
setLoading(true);
|
||||
remoteApi.queryProxyHomePage((resp) => {
|
||||
setLoading(false);
|
||||
if (resp.status === 0) {
|
||||
const { proxyAddrList, currentProxyAddr } = resp.data;
|
||||
setProxyAddrList(proxyAddrList || []);
|
||||
setSelectedProxy(currentProxyAddr || (proxyAddrList && proxyAddrList.length > 0 ? proxyAddrList[0] : ''));
|
||||
|
||||
if (currentProxyAddr) {
|
||||
localStorage.setItem('proxyAddr', currentProxyAddr);
|
||||
} else if (proxyAddrList && proxyAddrList.length > 0) {
|
||||
localStorage.setItem('proxyAddr', proxyAddrList[0]);
|
||||
}
|
||||
|
||||
} else {
|
||||
notificationApi.error({ message: resp.errMsg || t.FETCH_PROXY_LIST_FAILED, duration: 2 });
|
||||
}
|
||||
});
|
||||
}, [t]);
|
||||
|
||||
const handleSelectChange = (value) => {
|
||||
setSelectedProxy(value);
|
||||
localStorage.setItem('proxyAddr', value);
|
||||
};
|
||||
|
||||
|
||||
const handleAddProxyAddr = () => {
|
||||
if (!newProxyAddr.trim()) {
|
||||
notificationApi.warning({ message: t.INPUT_PROXY_ADDR_REQUIRED || "Please input a new proxy address.", duration: 2 });
|
||||
return;
|
||||
}
|
||||
setLoading(true);
|
||||
remoteApi.addProxyAddr(newProxyAddr.trim(), (resp) => {
|
||||
setLoading(false);
|
||||
if (resp.status === 0) {
|
||||
if (!proxyAddrList.includes(newProxyAddr.trim())) {
|
||||
setProxyAddrList(prevList => [...prevList, newProxyAddr.trim()]);
|
||||
}
|
||||
setNewProxyAddr('');
|
||||
notificationApi.info({ message: t.SUCCESS || "SUCCESS", duration: 2 });
|
||||
} else {
|
||||
notificationApi.error({ message: resp.errMsg || t.ADD_PROXY_FAILED, duration: 2 });
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
return (
|
||||
<Spin spinning={loading} tip={t.LOADING}>
|
||||
<div className="container-fluid" style={{ padding: '24px' }} id="deployHistoryList">
|
||||
<Card
|
||||
title={
|
||||
<div style={{ fontSize: '20px', fontWeight: 'bold' }}>
|
||||
ProxyServerAddressList
|
||||
</div>
|
||||
}
|
||||
bordered={false}
|
||||
>
|
||||
<Row gutter={[16, 16]} align="middle">
|
||||
<Col flex="auto" style={{ minWidth: 300, maxWidth: 500 }}>
|
||||
<Select
|
||||
style={{ width: '100%' }}
|
||||
value={selectedProxy}
|
||||
onChange={handleSelectChange}
|
||||
placeholder={t.SELECT}
|
||||
showSearch
|
||||
filterOption={(input, option) =>
|
||||
option.children.toLowerCase().indexOf(input.toLowerCase()) >= 0
|
||||
}
|
||||
>
|
||||
{proxyAddrList.map(addr => (
|
||||
<Option key={addr} value={addr}>
|
||||
{addr}
|
||||
</Option>
|
||||
))}
|
||||
</Select>
|
||||
</Col>
|
||||
</Row>
|
||||
|
||||
{writeOperationEnabled && (
|
||||
<Row gutter={[16, 16]} align="middle" style={{ marginTop: 16 }}>
|
||||
<Col>
|
||||
<label htmlFor="newProxyAddrInput">ProxyAddr:</label>
|
||||
</Col>
|
||||
<Col>
|
||||
<Input
|
||||
id="newProxyAddrInput"
|
||||
style={{ width: 300 }}
|
||||
value={newProxyAddr}
|
||||
onChange={(e) => setNewProxyAddr(e.target.value)}
|
||||
placeholder={t.INPUT_PROXY_ADDR}
|
||||
/>
|
||||
</Col>
|
||||
<Col>
|
||||
<Button type="primary" onClick={handleAddProxyAddr}>
|
||||
{t.ADD}
|
||||
</Button>
|
||||
</Col>
|
||||
</Row>
|
||||
)}
|
||||
</Card>
|
||||
|
||||
<Modal
|
||||
open={showModal}
|
||||
onCancel={() => setShowModal(false)}
|
||||
title={`${t.PROXY_CONFIG} [${selectedProxy}]`}
|
||||
footer={
|
||||
<div style={{ textAlign: 'center' }}>
|
||||
<Button onClick={() => setShowModal(false)}>{t.CLOSE}</Button>
|
||||
</div>
|
||||
}
|
||||
width={800}
|
||||
bodyStyle={{ maxHeight: '60vh', overflowY: 'auto' }}
|
||||
>
|
||||
<table className="table table-bordered" style={{ width: '100%' }}>
|
||||
<tbody>
|
||||
{Object.entries(allProxyConfig).length > 0 ? (
|
||||
Object.entries(allProxyConfig).map(([key, value]) => (
|
||||
<tr key={key}>
|
||||
<td style={{ fontWeight: 500, width: '30%' }}>{key}</td>
|
||||
<td>{value}</td>
|
||||
</tr>
|
||||
))
|
||||
) : (
|
||||
<tr>
|
||||
<td colSpan="2" style={{ textAlign: 'center' }}>{t.NO_CONFIG_DATA || "No configuration data available."}</td>
|
||||
</tr>
|
||||
)}
|
||||
</tbody>
|
||||
</table>
|
||||
</Modal>
|
||||
</div>
|
||||
</Spin>
|
||||
);
|
||||
};
|
||||
|
||||
export default ProxyManager;
|
725
frontend-new/src/pages/Topic/topic.jsx
Normal file
725
frontend-new/src/pages/Topic/topic.jsx
Normal file
@@ -0,0 +1,725 @@
|
||||
/*
|
||||
* Licensed to the Apache Software Foundation (ASF) under one or more
|
||||
* contributor license agreements. See the NOTICE file distributed with
|
||||
* this work for additional information regarding copyright ownership.
|
||||
* The ASF licenses this file to You under the Apache License, Version 2.0
|
||||
* (the "License"); you may not use this file except in compliance with
|
||||
* the License. You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import React, {useEffect, useState} from 'react';
|
||||
import {Button, Checkbox, Form, Input, message, Popconfirm, Space, Table} from 'antd';
|
||||
import {useLanguage} from '../../i18n/LanguageContext';
|
||||
import {remoteApi} from '../../api/remoteApi/remoteApi';
|
||||
import ResetOffsetResultDialog from "../../components/topic/ResetOffsetResultDialog";
|
||||
import SendResultDialog from "../../components/topic/SendResultDialog";
|
||||
import TopicModifyDialog from "../../components/topic/TopicModifyDialog";
|
||||
import ConsumerViewDialog from "../../components/topic/ConsumerViewDialog";
|
||||
import ConsumerResetOffsetDialog from "../../components/topic/ConsumerResetOffsetDialog";
|
||||
import SkipMessageAccumulateDialog from "../../components/topic/SkipMessageAccumulateDialog";
|
||||
import StatsViewDialog from "../../components/topic/StatsViewDialog";
|
||||
import RouterViewDialog from "../../components/topic/RouterViewDialog";
|
||||
import SendTopicMessageDialog from "../../components/topic/SendTopicMessageDialog";
|
||||
|
||||
|
||||
const DeployHistoryList = () => {
|
||||
const {t} = useLanguage();
|
||||
const [filterStr, setFilterStr] = useState('');
|
||||
const [filterNormal, setFilterNormal] = useState(true);
|
||||
const [filterDelay, setFilterDelay] = useState(false);
|
||||
const [filterFifo, setFilterFifo] = useState(false);
|
||||
const [filterTransaction, setFilterTransaction] = useState(false);
|
||||
const [filterUnspecified, setFilterUnspecified] = useState(false);
|
||||
const [filterRetry, setFilterRetry] = useState(false);
|
||||
const [filterDLQ, setFilterDLQ] = useState(false);
|
||||
const [filterSystem, setFilterSystem] = useState(false);
|
||||
const [rmqVersion, setRmqVersion] = useState(true);
|
||||
const [writeOperationEnabled, setWriteOperationEnabled] = useState(true);
|
||||
|
||||
const [allTopicList, setAllTopicList] = useState([]);
|
||||
const [allMessageTypeList, setAllMessageTypeList] = useState([]);
|
||||
const [topicShowList, setTopicShowList] = useState([]);
|
||||
const [loading, setLoading] = useState(false);
|
||||
|
||||
// Dialog visibility states
|
||||
const [isAddUpdateTopicModalVisible, setIsAddUpdateTopicModalVisible] = useState(false);
|
||||
const [isResetOffsetResultModalVisible, setIsResetOffsetResultModalVisible] = useState(false);
|
||||
const [isSendResultModalVisible, setIsSendResultModalVisible] = useState(false);
|
||||
const [isConsumerViewModalVisible, setIsConsumerViewModalVisible] = useState(false);
|
||||
const [isConsumerResetOffsetModalVisible, setIsConsumerResetOffsetModalVisible] = useState(false);
|
||||
const [isSkipMessageAccumulateModalVisible, setIsSkipMessageAccumulateModalVisible] = useState(false);
|
||||
const [isStatsViewModalVisible, setIsStatsViewModalVisible] = useState(false);
|
||||
const [isRouterViewModalVisible, setIsRouterViewModalVisible] = useState(false);
|
||||
const [isSendTopicMessageModalVisible, setIsSendTopicMessageModalVisible] = useState(false);
|
||||
|
||||
// Data for dialogs
|
||||
const [currentTopicForDialogs, setCurrentTopicForDialogs] = useState('');
|
||||
const [isUpdateMode, setIsUpdateMode] = useState(false);
|
||||
const [resetOffsetResultData, setResetOffsetResultData] = useState(null);
|
||||
const [sendResultData, setSendResultData] = useState(null);
|
||||
const [consumerData, setConsumerData] = useState(null);
|
||||
const [allConsumerGroupList, setAllConsumerGroupList] = useState([]);
|
||||
const [statsData, setStatsData] = useState(null);
|
||||
const [routeData, setRouteData] = useState(null);
|
||||
const [topicModifyData, setTopicModifyData] = useState([]);
|
||||
const [sendTopicMessageData, setSendTopicMessageData] = useState({
|
||||
topic: '',
|
||||
tag: '',
|
||||
key: '',
|
||||
messageBody: '',
|
||||
traceEnabled: false,
|
||||
});
|
||||
const [selectedConsumerGroups, setSelectedConsumerGroups] = useState([]);
|
||||
const [resetOffsetTime, setResetOffsetTime] = useState(new Date());
|
||||
|
||||
const [allClusterNameList, setAllClusterNameList] = useState([]);
|
||||
const [allBrokerNameList, setAllBrokerNameList] = useState([]);
|
||||
const [messageApi, msgContextHolder] = message.useMessage();
|
||||
// Pagination config
|
||||
const [paginationConf, setPaginationConf] = useState({
|
||||
current: 1,
|
||||
pageSize: 10,
|
||||
total: 0,
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
getTopicList();
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
filterList(paginationConf.current);
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [filterStr, filterNormal, filterDelay, filterFifo, filterTransaction,
|
||||
filterUnspecified, filterRetry, filterDLQ, filterSystem, allTopicList]);
|
||||
|
||||
// Close functions for Modals
|
||||
const closeAddUpdateDialog = () => {
|
||||
setIsAddUpdateTopicModalVisible(false);
|
||||
setTopicModifyData([]);
|
||||
};
|
||||
|
||||
const closeResetOffsetResultDialog = () => {
|
||||
setIsResetOffsetResultModalVisible(false);
|
||||
setResetOffsetResultData(null);
|
||||
};
|
||||
|
||||
const closeSendResultDialog = () => {
|
||||
setIsSendResultModalVisible(false);
|
||||
setSendResultData(null);
|
||||
};
|
||||
|
||||
const closeConsumerViewDialog = () => {
|
||||
setIsConsumerViewModalVisible(false);
|
||||
setConsumerData(null);
|
||||
setAllConsumerGroupList([]);
|
||||
};
|
||||
|
||||
const closeConsumerResetOffsetDialog = () => {
|
||||
setIsConsumerResetOffsetModalVisible(false);
|
||||
setSelectedConsumerGroups([]);
|
||||
setResetOffsetTime(new Date());
|
||||
setAllConsumerGroupList([]);
|
||||
};
|
||||
|
||||
const closeSkipMessageAccumulateDialog = () => {
|
||||
setIsSkipMessageAccumulateModalVisible(false);
|
||||
setSelectedConsumerGroups([]);
|
||||
setAllConsumerGroupList([]);
|
||||
};
|
||||
|
||||
const closeStatsViewDialog = () => {
|
||||
setIsStatsViewModalVisible(false);
|
||||
setStatsData(null);
|
||||
};
|
||||
|
||||
const closeRouterViewDialog = () => {
|
||||
setIsRouterViewModalVisible(false);
|
||||
setRouteData(null);
|
||||
};
|
||||
|
||||
const closeSendTopicMessageDialog = () => {
|
||||
setIsSendTopicMessageModalVisible(false);
|
||||
setSendTopicMessageData({topic: '', tag: '', key: '', messageBody: '', traceEnabled: false});
|
||||
};
|
||||
|
||||
const getTopicList = async () => {
|
||||
setLoading(true);
|
||||
try {
|
||||
const result = await remoteApi.queryTopicList();
|
||||
if (result.status === 0) {
|
||||
setAllTopicList(result.data.topicNameList);
|
||||
setAllMessageTypeList(result.data.messageTypeList);
|
||||
setPaginationConf(prev => ({
|
||||
...prev,
|
||||
total: result.data.topicNameList.length
|
||||
}));
|
||||
} else {
|
||||
messageApi.error(result.errMsg);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("Error fetching topic list:", error);
|
||||
messageApi.error("Failed to fetch topic list");
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
const refreshTopicList = async () => {
|
||||
setLoading(true);
|
||||
try {
|
||||
const result = await remoteApi.refreshTopicList();
|
||||
if (result.status === 0) {
|
||||
setAllTopicList(result.data.topicNameList);
|
||||
setAllMessageTypeList(result.data.messageTypeList);
|
||||
setPaginationConf(prev => ({
|
||||
...prev,
|
||||
total: result.data.topicNameList.length
|
||||
}));
|
||||
messageApi.success(t.REFRESHING_TOPIC_LIST);
|
||||
} else {
|
||||
messageApi.error(result.errMsg);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("Error refreshing topic list:", error);
|
||||
messageApi.error("Failed to refresh topic list");
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
const filterList = (currentPage) => {
|
||||
const lowExceptStr = filterStr.toLowerCase();
|
||||
const canShowList = allTopicList.filter((topic, index) => {
|
||||
if (filterStr && !topic.toLowerCase().includes(lowExceptStr)) {
|
||||
return false;
|
||||
}
|
||||
return filterByType(topic, allMessageTypeList[index]);
|
||||
});
|
||||
|
||||
const perPage = paginationConf.pageSize;
|
||||
const from = (currentPage - 1) * perPage;
|
||||
const to = (from + perPage) > canShowList.length ? canShowList.length : from + perPage;
|
||||
|
||||
setTopicShowList(canShowList.slice(from, to));
|
||||
setPaginationConf(prev => ({
|
||||
...prev,
|
||||
current: currentPage,
|
||||
total: canShowList.length
|
||||
}));
|
||||
};
|
||||
|
||||
const filterByType = (topic, type) => {
|
||||
if (filterRetry && type.includes("RETRY")) return true;
|
||||
if (filterDLQ && type.includes("DLQ")) return true;
|
||||
if (filterSystem && type.includes("SYSTEM")) return true;
|
||||
if (rmqVersion && filterUnspecified && type.includes("UNSPECIFIED")) return true;
|
||||
if (filterNormal && type.includes("NORMAL")) return true;
|
||||
if (!rmqVersion && filterNormal && type.includes("UNSPECIFIED")) return true;
|
||||
if (rmqVersion && filterDelay && type.includes("DELAY")) return true;
|
||||
if (rmqVersion && filterFifo && type.includes("FIFO")) return true;
|
||||
if (rmqVersion && filterTransaction && type.includes("TRANSACTION")) return true;
|
||||
|
||||
return false;
|
||||
};
|
||||
|
||||
const handleTableChange = (pagination) => {
|
||||
setPaginationConf(pagination);
|
||||
filterList(pagination.current);
|
||||
};
|
||||
|
||||
const openAddUpdateDialog = async (topic, isSys) => {
|
||||
|
||||
setCurrentTopicForDialogs(typeof topic === 'string' ? topic : (topic && topic.name) || '');
|
||||
const isUpdate = typeof topic === 'string' && !!topic; // 如果 topic 是非空字符串,则认为是更新
|
||||
|
||||
setIsUpdateMode(isUpdate);
|
||||
|
||||
try {
|
||||
if (isUpdate) {
|
||||
// topic 已经是字符串
|
||||
const configResult = await remoteApi.getTopicConfig(topic);
|
||||
if (configResult.status === 0) {
|
||||
const dataToSet = Array.isArray(configResult.data) ? configResult.data : [configResult.data];
|
||||
setTopicModifyData(dataToSet.map(item => ({
|
||||
clusterNameList: [],
|
||||
brokerNameList: item.brokerNameList || [],
|
||||
topicName: item.topicName,
|
||||
messageType: item.messageType || 'NORMAL',
|
||||
writeQueueNums: item.writeQueueNums || 8,
|
||||
readQueueNums: item.readQueueNums || 8,
|
||||
perm: item.perm || 7,
|
||||
})));
|
||||
} else {
|
||||
messageApi.error(configResult.errMsg);
|
||||
return;
|
||||
}
|
||||
} else {
|
||||
setTopicModifyData([{
|
||||
clusterNameList: [],
|
||||
brokerNameList: [],
|
||||
topicName: '',
|
||||
messageType: 'NORMAL',
|
||||
writeQueueNums: 8,
|
||||
readQueueNums: 8,
|
||||
perm: 7,
|
||||
}]);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("Error opening add/update dialog:", error);
|
||||
messageApi.error("Failed to open dialog");
|
||||
return;
|
||||
}
|
||||
|
||||
if(!isUpdate){
|
||||
const clusterResult = await remoteApi.getClusterList();
|
||||
if (clusterResult.status === 0) {
|
||||
setAllClusterNameList(Object.keys(clusterResult.data.clusterInfo.clusterAddrTable));
|
||||
setAllBrokerNameList(Object.keys(clusterResult.data.brokerServer));
|
||||
} else {
|
||||
messageApi.error(clusterResult.errMsg);
|
||||
}
|
||||
}
|
||||
setIsAddUpdateTopicModalVisible(true);
|
||||
};
|
||||
|
||||
// Post Topic Request (Add/Update)
|
||||
const postTopicRequest = async (values) => {
|
||||
try {
|
||||
const result = await remoteApi.createOrUpdateTopic(values);
|
||||
if (result.status === 0) {
|
||||
messageApi.success(t.TOPIC_OPERATION_SUCCESS);
|
||||
closeAddUpdateDialog();
|
||||
if(!isUpdateMode) {
|
||||
refreshTopicList();
|
||||
}
|
||||
} else {
|
||||
messageApi.error(result.errMsg);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("Error creating/updating topic:", error);
|
||||
messageApi.error("Failed to create/update topic");
|
||||
}
|
||||
};
|
||||
|
||||
// Delete Topic
|
||||
const deleteTopic = async (topicToDelete) => {
|
||||
try {
|
||||
const result = await remoteApi.deleteTopic(topicToDelete);
|
||||
if (result.status === 0) {
|
||||
messageApi.success(`${t.TOPIC} [${topicToDelete}] ${t.DELETED_SUCCESSFULLY}`);
|
||||
setAllTopicList(allTopicList.filter(topic => topic !== topicToDelete));
|
||||
await refreshTopicList()
|
||||
} else {
|
||||
messageApi.error(result.errMsg);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("Error deleting topic:", error);
|
||||
messageApi.error("Failed to delete topic");
|
||||
}
|
||||
};
|
||||
|
||||
// Open Stats View Dialog
|
||||
const statsView = async (topic) => {
|
||||
setCurrentTopicForDialogs(topic);
|
||||
try {
|
||||
const result = await remoteApi.getTopicStats(topic);
|
||||
if (result.status === 0) {
|
||||
setStatsData(result.data);
|
||||
setIsStatsViewModalVisible(true);
|
||||
} else {
|
||||
messageApi.error(result.errMsg);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("Error fetching stats:", error);
|
||||
messageApi.error("Failed to fetch stats");
|
||||
}
|
||||
};
|
||||
|
||||
// Open Router View Dialog
|
||||
const routerView = async (topic) => {
|
||||
setCurrentTopicForDialogs(topic);
|
||||
try {
|
||||
const result = await remoteApi.getTopicRoute(topic);
|
||||
if (result.status === 0) {
|
||||
setRouteData(result.data);
|
||||
setIsRouterViewModalVisible(true);
|
||||
} else {
|
||||
messageApi.error(result.errMsg);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("Error fetching route:", error);
|
||||
messageApi.error("Failed to fetch route");
|
||||
}
|
||||
};
|
||||
|
||||
// Open Consumer View Dialog
|
||||
const consumerView = async (topic) => {
|
||||
setCurrentTopicForDialogs(topic);
|
||||
try {
|
||||
const result = await remoteApi.getTopicConsumers(topic);
|
||||
if (result.status === 0) {
|
||||
setConsumerData(result.data);
|
||||
setAllConsumerGroupList(Object.keys(result.data));
|
||||
setIsConsumerViewModalVisible(true);
|
||||
} else {
|
||||
messageApi.error(result.errMsg);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("Error fetching consumers:", error);
|
||||
messageApi.error("Failed to fetch consumers");
|
||||
}
|
||||
};
|
||||
|
||||
// Open Consumer Reset Offset Dialog
|
||||
const openConsumerResetOffsetDialog = async (topic) => {
|
||||
setCurrentTopicForDialogs(topic);
|
||||
try {
|
||||
const result = await remoteApi.getTopicConsumerGroups(topic);
|
||||
if (result.status === 0) {
|
||||
if (!result.data.groupList) {
|
||||
messageApi.error("No consumer groups found");
|
||||
return;
|
||||
}
|
||||
setAllConsumerGroupList(result.data.groupList);
|
||||
setIsConsumerResetOffsetModalVisible(true);
|
||||
} else {
|
||||
messageApi.error(result.errMsg);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("Error fetching consumer groups:", error);
|
||||
messageApi.error("Failed to fetch consumer groups");
|
||||
}
|
||||
};
|
||||
|
||||
// Open Skip Message Accumulate Dialog
|
||||
const openSkipMessageAccumulateDialog = async (topic) => {
|
||||
setCurrentTopicForDialogs(topic);
|
||||
try {
|
||||
const result = await remoteApi.getTopicConsumerGroups(topic);
|
||||
if (result.status === 0) {
|
||||
if (!result.data.groupList) {
|
||||
messageApi.error("No consumer groups found");
|
||||
return;
|
||||
}
|
||||
setAllConsumerGroupList(result.data.groupList);
|
||||
setIsSkipMessageAccumulateModalVisible(true);
|
||||
} else {
|
||||
messageApi.error(result.errMsg);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("Error fetching consumer groups:", error);
|
||||
messageApi.error("Failed to fetch consumer groups");
|
||||
}
|
||||
};
|
||||
|
||||
// Open Send Topic Message Dialog
|
||||
const openSendTopicMessageDialog = (topic) => {
|
||||
setCurrentTopicForDialogs(topic);
|
||||
setSendTopicMessageData(prev => ({...prev, topic}));
|
||||
setIsSendTopicMessageModalVisible(true);
|
||||
};
|
||||
|
||||
const handleInputChange = (e) => {
|
||||
const {name, value} = e.target;
|
||||
setSendTopicMessageData(prevData => ({
|
||||
...prevData,
|
||||
[name]: value,
|
||||
}));
|
||||
};
|
||||
|
||||
const handleResetOffset = async (consumerGroupList, resetTime) => {
|
||||
try {
|
||||
const result = await remoteApi.resetConsumerOffset({
|
||||
resetTime: resetTime, // 使用传递过来的 resetTime
|
||||
consumerGroupList: consumerGroupList, // 使用传递过来的 consumerGroupList
|
||||
topic: currentTopicForDialogs,
|
||||
force: true
|
||||
});
|
||||
if (result.status === 0) {
|
||||
setResetOffsetResultData(result.data);
|
||||
setIsResetOffsetResultModalVisible(true);
|
||||
setIsConsumerResetOffsetModalVisible(false);
|
||||
} else {
|
||||
messageApi.error(result.errMsg);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("Error resetting offset:", error);
|
||||
messageApi.error("Failed to reset offset");
|
||||
}
|
||||
};
|
||||
|
||||
const handleSkipMessageAccumulate = async (consumerGroupListFromDialog) => {
|
||||
try {
|
||||
const result = await remoteApi.skipMessageAccumulate({
|
||||
resetTime: -1,
|
||||
consumerGroupList: consumerGroupListFromDialog, // 使用子组件传递的 consumerGroupList
|
||||
topic: currentTopicForDialogs, // 使用父组件中管理的 topic
|
||||
force: true
|
||||
});
|
||||
if (result.status === 0) {
|
||||
setResetOffsetResultData(result.data); // 注意这里使用了 setResetOffsetResultData,确认这是你期望的
|
||||
setIsResetOffsetResultModalVisible(true); // 注意这里使用了 setIsResetOffsetResultModalVisible,确认这是你期望的
|
||||
setIsSkipMessageAccumulateModalVisible(false);
|
||||
} else {
|
||||
messageApi.error(result.errMsg);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("Error skipping message accumulate:", error);
|
||||
messageApi.error("Failed to skip message accumulate");
|
||||
}
|
||||
};
|
||||
|
||||
const columns = [
|
||||
{
|
||||
title: t.TOPIC,
|
||||
dataIndex: 'topic',
|
||||
key: 'topic',
|
||||
align: 'center',
|
||||
render: (text) => {
|
||||
const sysFlag = text.startsWith('%SYS%');
|
||||
const topic = sysFlag ? text.substring(5) : text;
|
||||
return <span style={{color: sysFlag ? 'red' : ''}}>{topic}</span>;
|
||||
},
|
||||
},
|
||||
{
|
||||
title: t.OPERATION,
|
||||
key: 'operation',
|
||||
align: 'left',
|
||||
render: (_, record) => {
|
||||
const sysFlag = record.topic.startsWith('%SYS%');
|
||||
const topicName = sysFlag ? record.topic.substring(5) : record.topic;
|
||||
return (
|
||||
<Space size="small">
|
||||
<Button type="primary" size="small" onClick={() => statsView(topicName)}>
|
||||
{t.STATUS}
|
||||
</Button>
|
||||
<Button type="primary" size="small" onClick={() => routerView(topicName)}>
|
||||
{t.ROUTER}
|
||||
</Button>
|
||||
<Button type="primary" size="small" onClick={() => consumerView(topicName)}>
|
||||
Consumer {t.MANAGE}
|
||||
</Button>
|
||||
<Button type="primary" size="small" onClick={() => openAddUpdateDialog(topicName, sysFlag)}>
|
||||
Topic {t.CONFIG}
|
||||
</Button>
|
||||
{!sysFlag && (
|
||||
<Button type="primary" size="small" onClick={() => openSendTopicMessageDialog(topicName)}>
|
||||
{t.SEND_MSG}
|
||||
</Button>
|
||||
)}
|
||||
{!sysFlag && writeOperationEnabled && (
|
||||
<Button type="primary" danger size="small"
|
||||
onClick={() => openConsumerResetOffsetDialog(topicName)}>
|
||||
{t.RESET_CUS_OFFSET}
|
||||
</Button>
|
||||
)}
|
||||
{!sysFlag && writeOperationEnabled && (
|
||||
<Button type="primary" danger size="small"
|
||||
onClick={() => openSkipMessageAccumulateDialog(topicName)}>
|
||||
{t.SKIP_MESSAGE_ACCUMULATE}
|
||||
</Button>
|
||||
)}
|
||||
{!sysFlag && writeOperationEnabled && (
|
||||
<Popconfirm
|
||||
title={`${t.ARE_YOU_SURE_TO_DELETE}`}
|
||||
onConfirm={() => deleteTopic(topicName)}
|
||||
okText={t.YES}
|
||||
cancelText={t.NOT}
|
||||
>
|
||||
<Button type="primary" danger size="small">
|
||||
{t.DELETE}
|
||||
</Button>
|
||||
</Popconfirm>
|
||||
)}
|
||||
</Space>
|
||||
);
|
||||
},
|
||||
},
|
||||
];
|
||||
|
||||
return (
|
||||
<>
|
||||
{msgContextHolder}
|
||||
<div className="container-fluid" id="deployHistoryList">
|
||||
<div className="modal-body">
|
||||
<div className="row">
|
||||
<Form layout="inline" className="pull-left col-sm-12">
|
||||
<Form.Item label={t.TOPIC}>
|
||||
<Input
|
||||
value={filterStr}
|
||||
onChange={(e) => setFilterStr(e.target.value)}
|
||||
/>
|
||||
</Form.Item>
|
||||
<Form.Item>
|
||||
<Checkbox checked={filterNormal} onChange={(e) => setFilterNormal(e.target.checked)}>
|
||||
{t.NORMAL}
|
||||
</Checkbox>
|
||||
</Form.Item>
|
||||
{rmqVersion && (
|
||||
<>
|
||||
<Form.Item>
|
||||
<Checkbox checked={filterDelay}
|
||||
onChange={(e) => setFilterDelay(e.target.checked)}>
|
||||
{t.DELAY}
|
||||
</Checkbox>
|
||||
</Form.Item>
|
||||
<Form.Item>
|
||||
<Checkbox checked={filterFifo}
|
||||
onChange={(e) => setFilterFifo(e.target.checked)}>
|
||||
{t.FIFO}
|
||||
</Checkbox>
|
||||
</Form.Item>
|
||||
<Form.Item>
|
||||
<Checkbox checked={filterTransaction}
|
||||
onChange={(e) => setFilterTransaction(e.target.checked)}>
|
||||
{t.TRANSACTION}
|
||||
</Checkbox>
|
||||
</Form.Item>
|
||||
<Form.Item>
|
||||
<Checkbox checked={filterUnspecified}
|
||||
onChange={(e) => setFilterUnspecified(e.target.checked)}>
|
||||
{t.UNSPECIFIED}
|
||||
</Checkbox>
|
||||
</Form.Item>
|
||||
</>
|
||||
)}
|
||||
<Form.Item>
|
||||
<Checkbox checked={filterRetry} onChange={(e) => setFilterRetry(e.target.checked)}>
|
||||
{t.RETRY}
|
||||
</Checkbox>
|
||||
</Form.Item>
|
||||
<Form.Item>
|
||||
<Checkbox checked={filterDLQ} onChange={(e) => setFilterDLQ(e.target.checked)}>
|
||||
{t.DLQ}
|
||||
</Checkbox>
|
||||
</Form.Item>
|
||||
<Form.Item>
|
||||
<Checkbox checked={filterSystem} onChange={(e) => setFilterSystem(e.target.checked)}>
|
||||
{t.SYSTEM}
|
||||
</Checkbox>
|
||||
</Form.Item>
|
||||
{writeOperationEnabled && (
|
||||
<Form.Item>
|
||||
<Button type="primary" onClick={openAddUpdateDialog}>
|
||||
{t.ADD} / {t.UPDATE}
|
||||
</Button>
|
||||
</Form.Item>
|
||||
)}
|
||||
<Form.Item>
|
||||
<Button type="primary" onClick={refreshTopicList}>
|
||||
{t.REFRESH}
|
||||
</Button>
|
||||
</Form.Item>
|
||||
</Form>
|
||||
</div>
|
||||
<br/>
|
||||
<div>
|
||||
<div className="row">
|
||||
<Table
|
||||
bordered
|
||||
loading={loading}
|
||||
dataSource={topicShowList.map((topic, index) => ({key: index, topic}))}
|
||||
columns={columns}
|
||||
pagination={paginationConf}
|
||||
onChange={handleTableChange}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Modals/Dialogs - 传递 visible 和 onClose prop */}
|
||||
<ResetOffsetResultDialog
|
||||
visible={isResetOffsetResultModalVisible}
|
||||
onClose={closeResetOffsetResultDialog} // 传递关闭函数
|
||||
result={resetOffsetResultData}
|
||||
t={t}
|
||||
/>
|
||||
|
||||
<SendResultDialog
|
||||
visible={isSendResultModalVisible}
|
||||
onClose={closeSendResultDialog} // 传递关闭函数
|
||||
result={sendResultData}
|
||||
t={t}
|
||||
/>
|
||||
|
||||
<TopicModifyDialog
|
||||
visible={isAddUpdateTopicModalVisible}
|
||||
onClose={closeAddUpdateDialog}
|
||||
initialData={topicModifyData}
|
||||
bIsUpdate={isUpdateMode}
|
||||
writeOperationEnabled={writeOperationEnabled}
|
||||
allClusterNameList={allClusterNameList || []}
|
||||
allBrokerNameList={allBrokerNameList || []}
|
||||
onSubmit={postTopicRequest}
|
||||
onInputChange={handleInputChange}
|
||||
t={t}
|
||||
/>
|
||||
|
||||
<ConsumerViewDialog
|
||||
visible={isConsumerViewModalVisible}
|
||||
onClose={closeConsumerViewDialog} // 传递关闭函数
|
||||
topic={currentTopicForDialogs}
|
||||
consumerData={consumerData}
|
||||
consumerGroupCount={allConsumerGroupList.length}
|
||||
t={t}
|
||||
/>
|
||||
|
||||
<ConsumerResetOffsetDialog
|
||||
visible={isConsumerResetOffsetModalVisible}
|
||||
onClose={closeConsumerResetOffsetDialog} // 传递关闭函数
|
||||
topic={currentTopicForDialogs}
|
||||
allConsumerGroupList={allConsumerGroupList}
|
||||
handleResetOffset={handleResetOffset}
|
||||
t={t}
|
||||
/>
|
||||
|
||||
<SkipMessageAccumulateDialog
|
||||
visible={isSkipMessageAccumulateModalVisible}
|
||||
onClose={closeSkipMessageAccumulateDialog} // 传递关闭函数
|
||||
topic={currentTopicForDialogs}
|
||||
allConsumerGroupList={allConsumerGroupList}
|
||||
handleSkipMessageAccumulate={handleSkipMessageAccumulate}
|
||||
t={t}
|
||||
/>
|
||||
|
||||
<StatsViewDialog
|
||||
visible={isStatsViewModalVisible}
|
||||
onClose={closeStatsViewDialog} // 传递关闭函数
|
||||
topic={currentTopicForDialogs}
|
||||
statsData={statsData}
|
||||
t={t}
|
||||
/>
|
||||
|
||||
<RouterViewDialog
|
||||
visible={isRouterViewModalVisible}
|
||||
onClose={closeRouterViewDialog} // 传递关闭函数
|
||||
topic={currentTopicForDialogs}
|
||||
routeData={routeData}
|
||||
t={t}
|
||||
/>
|
||||
|
||||
<SendTopicMessageDialog
|
||||
visible={isSendTopicMessageModalVisible}
|
||||
onClose={closeSendTopicMessageDialog} // 传递关闭函数
|
||||
topic={currentTopicForDialogs}
|
||||
setSendResultData={setSendResultData}
|
||||
setIsSendResultModalVisible={setIsSendResultModalVisible}
|
||||
setIsSendTopicMessageModalVisible={setIsSendTopicMessageModalVisible}
|
||||
sendTopicMessageData={sendTopicMessageData}
|
||||
t={t}
|
||||
/>
|
||||
</div>
|
||||
</>
|
||||
|
||||
);
|
||||
};
|
||||
|
||||
export default DeployHistoryList;
|
Reference in New Issue
Block a user