This is an automated email from the ASF dual-hosted git repository.
jinrongtong pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/rocketmq-dashboard.git
The following commit(s) were added to refs/heads/master by this push:
new bd94e8c [GSOC][RIP-78][ISSUES#308] Add part of refactored front-end
files (#311)
bd94e8c is described below
commit bd94e8c4f53adc0decda96b714c8e6fefe7efa39
Author: Crazylychee <[email protected]>
AuthorDate: Mon Jun 16 13:47:34 2025 +0800
[GSOC][RIP-78][ISSUES#308] Add part of refactored front-end files (#311)
---
frontend-new/src/components/acl/ResourceInput.jsx | 127 ++++
frontend-new/src/components/acl/SubjectInput.jsx | 101 +++
.../src/components/consumer/ClientInfoModal.jsx | 103 +++
.../src/components/consumer/ConsumerConfigItem.jsx | 287 ++++++++
.../components/consumer/ConsumerConfigModal.jsx | 169 +++++
.../components/consumer/ConsumerDetailModal.jsx | 79 +++
.../components/consumer/DeleteConsumerModal.jsx | 110 ++++
.../components/topic/ConsumerResetOffsetDialog.jsx | 76 +++
.../src/components/topic/ConsumerViewDialog.jsx | 96 +++
.../components/topic/ResetOffsetResultDialog.jsx | 65 ++
.../src/components/topic/RouterViewDialog.jsx | 111 ++++
.../src/components/topic/SendResultDialog.jsx | 65 ++
.../components/topic/SendTopicMessageDialog.jsx | 102 +++
.../topic/SkipMessageAccumulateDialog.jsx | 70 ++
.../src/components/topic/StatsViewDialog.jsx | 66 ++
.../src/components/topic/TopicModifyDialog.jsx | 65 ++
.../src/components/topic/TopicSingleModifyForm.jsx | 144 ++++
frontend-new/src/pages/Producer/producer.jsx | 143 ++++
frontend-new/src/pages/Proxy/proxy.jsx | 181 +++++
frontend-new/src/pages/Topic/topic.jsx | 725 +++++++++++++++++++++
20 files changed, 2885 insertions(+)
diff --git a/frontend-new/src/components/acl/ResourceInput.jsx
b/frontend-new/src/components/acl/ResourceInput.jsx
new file mode 100644
index 0000000..280e1c3
--- /dev/null
+++ b/frontend-new/src/components/acl/ResourceInput.jsx
@@ -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;
diff --git a/frontend-new/src/components/acl/SubjectInput.jsx
b/frontend-new/src/components/acl/SubjectInput.jsx
new file mode 100644
index 0000000..bf9f649
--- /dev/null
+++ b/frontend-new/src/components/acl/SubjectInput.jsx
@@ -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;
diff --git a/frontend-new/src/components/consumer/ClientInfoModal.jsx
b/frontend-new/src/components/consumer/ClientInfoModal.jsx
new file mode 100644
index 0000000..ed4a5ca
--- /dev/null
+++ b/frontend-new/src/components/consumer/ClientInfoModal.jsx
@@ -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;
diff --git a/frontend-new/src/components/consumer/ConsumerConfigItem.jsx
b/frontend-new/src/components/consumer/ConsumerConfigItem.jsx
new file mode 100644
index 0000000..cd083c6
--- /dev/null
+++ b/frontend-new/src/components/consumer/ConsumerConfigItem.jsx
@@ -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;
+
diff --git a/frontend-new/src/components/consumer/ConsumerConfigModal.jsx
b/frontend-new/src/components/consumer/ConsumerConfigModal.jsx
new file mode 100644
index 0000000..a58e981
--- /dev/null
+++ b/frontend-new/src/components/consumer/ConsumerConfigModal.jsx
@@ -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;
diff --git a/frontend-new/src/components/consumer/ConsumerDetailModal.jsx
b/frontend-new/src/components/consumer/ConsumerDetailModal.jsx
new file mode 100644
index 0000000..d4b2367
--- /dev/null
+++ b/frontend-new/src/components/consumer/ConsumerDetailModal.jsx
@@ -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;
diff --git a/frontend-new/src/components/consumer/DeleteConsumerModal.jsx
b/frontend-new/src/components/consumer/DeleteConsumerModal.jsx
new file mode 100644
index 0000000..e9e3a1b
--- /dev/null
+++ b/frontend-new/src/components/consumer/DeleteConsumerModal.jsx
@@ -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;
diff --git a/frontend-new/src/components/topic/ConsumerResetOffsetDialog.jsx
b/frontend-new/src/components/topic/ConsumerResetOffsetDialog.jsx
new file mode 100644
index 0000000..e5056d4
--- /dev/null
+++ b/frontend-new/src/components/topic/ConsumerResetOffsetDialog.jsx
@@ -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;
diff --git a/frontend-new/src/components/topic/ConsumerViewDialog.jsx
b/frontend-new/src/components/topic/ConsumerViewDialog.jsx
new file mode 100644
index 0000000..304628a
--- /dev/null
+++ b/frontend-new/src/components/topic/ConsumerViewDialog.jsx
@@ -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;
diff --git a/frontend-new/src/components/topic/ResetOffsetResultDialog.jsx
b/frontend-new/src/components/topic/ResetOffsetResultDialog.jsx
new file mode 100644
index 0000000..4984773
--- /dev/null
+++ b/frontend-new/src/components/topic/ResetOffsetResultDialog.jsx
@@ -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;
diff --git a/frontend-new/src/components/topic/RouterViewDialog.jsx
b/frontend-new/src/components/topic/RouterViewDialog.jsx
new file mode 100644
index 0000000..aebb899
--- /dev/null
+++ b/frontend-new/src/components/topic/RouterViewDialog.jsx
@@ -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;
diff --git a/frontend-new/src/components/topic/SendResultDialog.jsx
b/frontend-new/src/components/topic/SendResultDialog.jsx
new file mode 100644
index 0000000..6dd3208
--- /dev/null
+++ b/frontend-new/src/components/topic/SendResultDialog.jsx
@@ -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;
diff --git a/frontend-new/src/components/topic/SendTopicMessageDialog.jsx
b/frontend-new/src/components/topic/SendTopicMessageDialog.jsx
new file mode 100644
index 0000000..7b9a010
--- /dev/null
+++ b/frontend-new/src/components/topic/SendTopicMessageDialog.jsx
@@ -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;
diff --git a/frontend-new/src/components/topic/SkipMessageAccumulateDialog.jsx
b/frontend-new/src/components/topic/SkipMessageAccumulateDialog.jsx
new file mode 100644
index 0000000..cd28a6e
--- /dev/null
+++ b/frontend-new/src/components/topic/SkipMessageAccumulateDialog.jsx
@@ -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;
diff --git a/frontend-new/src/components/topic/StatsViewDialog.jsx
b/frontend-new/src/components/topic/StatsViewDialog.jsx
new file mode 100644
index 0000000..2f6f51f
--- /dev/null
+++ b/frontend-new/src/components/topic/StatsViewDialog.jsx
@@ -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;
diff --git a/frontend-new/src/components/topic/TopicModifyDialog.jsx
b/frontend-new/src/components/topic/TopicModifyDialog.jsx
new file mode 100644
index 0000000..523d9f3
--- /dev/null
+++ b/frontend-new/src/components/topic/TopicModifyDialog.jsx
@@ -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;
diff --git a/frontend-new/src/components/topic/TopicSingleModifyForm.jsx
b/frontend-new/src/components/topic/TopicSingleModifyForm.jsx
new file mode 100644
index 0000000..b4288e4
--- /dev/null
+++ b/frontend-new/src/components/topic/TopicSingleModifyForm.jsx
@@ -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;
diff --git a/frontend-new/src/pages/Producer/producer.jsx
b/frontend-new/src/pages/Producer/producer.jsx
new file mode 100644
index 0000000..28a2d69
--- /dev/null
+++ b/frontend-new/src/pages/Producer/producer.jsx
@@ -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;
diff --git a/frontend-new/src/pages/Proxy/proxy.jsx
b/frontend-new/src/pages/Proxy/proxy.jsx
new file mode 100644
index 0000000..ab9a359
--- /dev/null
+++ b/frontend-new/src/pages/Proxy/proxy.jsx
@@ -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;
diff --git a/frontend-new/src/pages/Topic/topic.jsx
b/frontend-new/src/pages/Topic/topic.jsx
new file mode 100644
index 0000000..5363a2b
--- /dev/null
+++ b/frontend-new/src/pages/Topic/topic.jsx
@@ -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;