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;

Reply via email to