From 3cbff604e6a552662a472b921ba033fed012a357 Mon Sep 17 00:00:00 2001
From: Crazylychee <110229037+Crazylychee@users.noreply.github.com>
Date: Mon, 16 Jun 2025 13:51:18 +0800
Subject: [PATCH] [GSOC][RIP-78][ISSUES#308] Add part of refactored front-end
files (#312)
---
frontend-new/src/pages/Acl/acl.jsx | 676 +++++++++++++++++
frontend-new/src/pages/Cluster/cluster.jsx | 303 ++++++++
frontend-new/src/pages/Consumer/consumer.jsx | 480 ++++++++++++
.../src/pages/Dashboard/DashboardPage.jsx | 455 ++++++++++++
.../src/pages/DlqMessage/dlqmessage.jsx | 703 ++++++++++++++++++
frontend-new/src/pages/Login/login.jsx | 90 +++
frontend-new/src/pages/Message/message.jsx | 478 ++++++++++++
.../src/pages/MessageTrace/messagetrace.jsx | 429 +++++++++++
frontend-new/src/pages/Ops/ops.jsx | 183 +++++
9 files changed, 3797 insertions(+)
create mode 100644 frontend-new/src/pages/Acl/acl.jsx
create mode 100644 frontend-new/src/pages/Cluster/cluster.jsx
create mode 100644 frontend-new/src/pages/Consumer/consumer.jsx
create mode 100644 frontend-new/src/pages/Dashboard/DashboardPage.jsx
create mode 100644 frontend-new/src/pages/DlqMessage/dlqmessage.jsx
create mode 100644 frontend-new/src/pages/Login/login.jsx
create mode 100644 frontend-new/src/pages/Message/message.jsx
create mode 100644 frontend-new/src/pages/MessageTrace/messagetrace.jsx
create mode 100644 frontend-new/src/pages/Ops/ops.jsx
diff --git a/frontend-new/src/pages/Acl/acl.jsx b/frontend-new/src/pages/Acl/acl.jsx
new file mode 100644
index 0000000..06aa87a
--- /dev/null
+++ b/frontend-new/src/pages/Acl/acl.jsx
@@ -0,0 +1,676 @@
+/*
+ * 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 {
+ Table,
+ Button,
+ Input,
+ Tabs,
+ Modal,
+ Form,
+ message,
+ Space,
+ Tag,
+ Popconfirm,
+ Select
+} from 'antd';
+import {
+ EditOutlined,
+ DeleteOutlined,
+ EyeOutlined,
+ EyeInvisibleOutlined
+} from '@ant-design/icons';
+import {remoteApi} from "../../api/remoteApi/remoteApi";
+import ResourceInput from '../../components/acl/ResourceInput';
+import SubjectInput from "../../components/acl/SubjectInput";
+import {useLanguage} from "../../i18n/LanguageContext";
+const { TabPane } = Tabs;
+const { Search } = Input;
+
+const Acl = () => {
+ const [activeTab, setActiveTab] = useState('users');
+ const [userListData, setUserListData] = useState([]);
+ const [aclListData, setAclListData] = useState([]);
+ const [loading, setLoading] = useState(false);
+ const [searchText, setSearchText] = useState('');
+
+ const [isUserModalVisible, setIsUserModalVisible] = useState(false);
+ const [currentUser, setCurrentUser] = useState(null);
+ const [userForm] = Form.useForm();
+ const [showPassword, setShowPassword] = useState(false);
+
+ const [isAclModalVisible, setIsAclModalVisible] = useState(false);
+ const [currentAcl, setCurrentAcl] = useState(null);
+ const [aclForm] = Form.useForm();
+ const [messageApi, msgContextHolder] = message.useMessage();
+ const [isUpdate, setIsUpdate] = useState(false);
+ const [ips, setIps] = useState([]);
+ const {t} = useLanguage();
+ // 校验IP地址的正则表达式
+ const ipRegex =
+ /^(?:(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.){3}(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)$|^((?:[0-9A-Fa-f]{1,4}:){7}[0-9A-Fa-f]{1,4}|(?:[0-9A-Fa-f]{1,4}:){6}[0-9A-Fa-f]{1,4}(?::[0-9A-Fa-f]{1,4}){0,1}|(?:[0-9A-Fa-f]{1,4}:){5}[0-9A-Fa-f]{1,4}(?::[0-9A-Fa-f]{1,4}){0,2}|(?:[0-9A-Fa-f]{1,4}:){4}[0-9A-Fa-f]{1,4}(?::[0-9A-Fa-f]{1,4}){0,3}|(?:[0-9A-Fa-f]{1,4}:){3}[0-9A-Fa-f]{1,4}(?::[0-9A-Fa-f]{1,4}){0,4}|(?:[0-9A-Fa-f]{1,4}:){2}[0-9A-Fa-f]{1,4}(?::[0-9A-Fa-f]{1,4}){0,5}|(?:[0-9A-Fa-f]{1,4}:){1}[0-9A-Fa-f]{1,4}(?::[0-9A-Fa-f]{1,4}){0,6}|(?::(?::[0-9A-Fa-f]{1,4}){1,7}|::))(\/(?:12[0-7]|1[0-1][0-9]|[1-9]?[0-9]))?$/;
+ // 支持 IPv4 和 IPv6,包括 CIDR 表示法
+
+ const handleIpChange = value => {
+ // 过滤掉重复的IP地址
+ const uniqueIps = Array.from(new Set(value));
+ setIps(uniqueIps);
+ };
+
+ const handleIpDeselect = value => {
+ // 移除被取消选择的IP
+ setIps(ips.filter(ip => ip !== value));
+ };
+
+ const validateIp = (rule, value) => {
+ if (!value || value.length === 0) {
+ return Promise.resolve(); // Allow empty
+ }
+ const invalidIps = value.filter(ip => !ipRegex.test(ip));
+ if (invalidIps.length > 0) {
+ return Promise.reject(t.INVALID_IP_ADDRESSES +"ips:" + invalidIps.join(', '));
+ }
+ return Promise.resolve();
+ };
+
+// --- Data Loading Functions ---
+ const fetchUsers = async () => {
+ setLoading(true);
+ try {
+ const result = await remoteApi.listUsers();
+ if (result && result.status === 0 && result.data) {
+ const formattedUsers = result.data.map(user => ({
+ ...user,
+ key: user.username, // Table needs key
+ userStatus: user.userStatus === 'enable' ? t.ENABLED : t.DISABLED // Format status
+ }));
+ setUserListData(formattedUsers);
+ } else {
+ messageApi.error(t.GET_USERS_FAILED+result?.errMsg);
+ }
+ } catch (error) {
+ console.error("Failed to fetch users:", error);
+ messageApi.error(t.GET_USERS_EXCEPTION);
+ } finally {
+ setLoading(false);
+ }
+ };
+
+ const fetchAcls = async (value) => {
+ setLoading(true);
+ try {
+ const result = await remoteApi.listAcls(null, value);
+ if (result && result.status === 0) {
+ const formattedAcls = [];
+
+ if (result && result.data && Array.isArray(result.data)) {
+ result.data.forEach((acl, aclIndex) => {
+ const subject = acl.subject;
+
+ if (acl.policies && Array.isArray(acl.policies)) {
+ acl.policies.forEach((policy, policyIndex) => {
+ const policyType = policy.policyType;
+
+ if (policy.entries && Array.isArray(policy.entries)) {
+ policy.entries.forEach((entry, entryIndex) => {
+ const resources = Array.isArray(entry.resource) ? entry.resource : (entry.resource ? [entry.resource] : []);
+
+ resources.forEach((singleResource, resourceIndex) => {
+ console.log(singleResource)
+ formattedAcls.push({
+ key: `acl-${aclIndex}-policy-${policyIndex}-entry-${entryIndex}-resource-${singleResource}`,
+ subject: subject,
+ policyType: policyType,
+ resource: singleResource || t.N_A,
+ actions: (entry.actions && Array.isArray(entry.actions)) ? entry.actions.join(', ') : '',
+ sourceIps: (entry.sourceIps && Array.isArray(entry.sourceIps)) ? entry.sourceIps.join(', ') : t.N_A,
+ decision: entry.decision || t.N_A
+ });
+ });
+ });
+ }
+ });
+ }
+ });
+ } else {
+ console.warn(t.INVALID_OR_EMPTY_ACL_DATA);
+ }
+ setAclListData(formattedAcls);
+ } else {
+ messageApi.error(t.GET_ACLS_FAILED + result?.errMsg);
+ }
+ } catch (error) {
+ console.error("Failed to fetch ACLs:", error);
+ messageApi.error(t.GET_ACLS_EXCEPTION);
+ } finally {
+ setLoading(false);
+ }
+ };
+
+ useEffect(() => {
+ if (activeTab === 'users') {
+ fetchUsers();
+ } else {
+ fetchAcls();
+ }
+ }, [activeTab]);
+
+// --- User Management Logic ---
+
+ const handleAddUser = () => {
+ setCurrentUser(null);
+ userForm.resetFields();
+ setShowPassword(false);
+ setIsUserModalVisible(true);
+ };
+
+ const handleEditUser = (record) => {
+ setCurrentUser(record);
+ userForm.setFieldsValue({
+ username: record.username,
+ password: record.password,
+ userType: record.userType,
+ userStatus: record.userStatus === t.ENABLED ? 'enable' : 'disable'
+ });
+ setShowPassword(false);
+ setIsUserModalVisible(true);
+ };
+
+ const handleDeleteUser = async (username) => {
+ setLoading(true);
+ try {
+ const result = await remoteApi.deleteUser(null, username);
+ if (result.status === 0) {
+ messageApi.success(t.USER_DELETE_SUCCESS);
+ fetchUsers();
+ } else {
+ messageApi.error(t.USER_DELETE_FAILED + result.errMsg);
+ }
+ } catch (error) {
+ console.error("Failed to delete user:", error);
+ messageApi.error(t.USER_DELETE_EXCEPTION);
+ } finally {
+ setLoading(false);
+ }
+ };
+
+ const handleUserModalOk = async () => {
+ try {
+ const values = await userForm.validateFields();
+ setLoading(true);
+ let result;
+
+ const userInfoParam = {
+ username: values.username,
+ password: values.password,
+ userType: values.userType,
+ userStatus: values.userStatus,
+ };
+
+ if (currentUser) {
+ result = await remoteApi.updateUser(null, userInfoParam);
+ if (result.status === 0) {
+ messageApi.success(t.USER_UPDATE_SUCCESS);
+ } else {
+ messageApi.error(result.errMsg);
+ }
+ } else {
+ result = await remoteApi.createUser(null, userInfoParam);
+ if (result.status === 0) {
+ messageApi.success(t.USER_CREATE_SUCCESS);
+ } else {
+ messageApi.error(result.errMsg);
+ }
+ }
+ setIsUserModalVisible(false);
+ fetchUsers();
+ } catch (error) {
+ console.error("Failed to save user:", error);
+ messageApi.error(t.SAVE_USER_FAILED);
+ } finally {
+ setLoading(false);
+ }
+ };
+
+// --- ACL Permission Management Logic ---
+ const handleAddAcl = () => {
+ setCurrentAcl(null);
+ setIsUpdate(false)
+ aclForm.resetFields();
+ setIsAclModalVisible(true);
+ };
+
+ const handleEditAcl = (record) => {
+ setCurrentAcl(record);
+ setIsUpdate(true);
+ aclForm.setFieldsValue({
+ subject: record.subject,
+ policyType: record.policyType,
+ resource: record.resource,
+ actions: record.actions ? record.actions.split(', ') : [],
+ sourceIps: record.sourceIps ? record.sourceIps.split(', ') : [],
+ decision: record.decision
+ });
+ setIsAclModalVisible(true);
+ };
+
+ const handleDeleteAcl = async (subject, resource) => {
+ setLoading(true);
+ try {
+ const result = await remoteApi.deleteAcl(null, subject, resource);
+ if (result.status === 0) {
+ messageApi.success(t.ACL_DELETE_SUCCESS);
+ fetchAcls();
+ } else {
+ messageApi.error(t.ACL_DELETE_FAILED+result.errMsg);
+ }
+ } catch (error) {
+ console.error("Failed to delete ACL:", error);
+ messageApi.error(t.ACL_DELETE_EXCEPTION);
+ } finally {
+ setLoading(false);
+ }
+ };
+
+ const handleAclModalOk = async () => {
+ try {
+ const values = await aclForm.validateFields();
+ setLoading(true);
+ let result;
+
+ const policiesParam = [
+ {
+ policyType: values.policyType,
+ entries: [
+ {
+ resource: isUpdate ? [values.resource] : values.resource,
+ actions: values.actions,
+ sourceIps: values.sourceIps,
+ decision: values.decision
+ }
+ ]
+ }
+ ];
+
+ if (isUpdate) { // This condition seems reversed for update/create based on the current logic.
+ result = await remoteApi.updateAcl(null, values.subject, policiesParam);
+ if (result.status === 0) {
+ messageApi.success(t.ACL_UPDATE_SUCCESS);
+ setIsAclModalVisible(false);
+ fetchAcls();
+ } else {
+ messageApi.error(t.ACL_UPDATE_FAILED+result.errMsg);
+ }
+ setIsUpdate(false)
+ } else {
+ result = await remoteApi.createAcl(null, values.subject, policiesParam);
+ console.log(result)
+ if (result.status === 0) {
+ messageApi.success(t.ACL_CREATE_SUCCESS);
+ setIsAclModalVisible(false);
+ fetchAcls();
+ } else {
+ messageApi.error(t.ACL_CREATE_FAILED+result.errMsg);
+ }
+ }
+
+ } catch (error) {
+ console.error("Failed to save ACL:", error);
+ messageApi.error(t.SAVE_ACL_FAILED);
+ } finally {
+ setLoading(false);
+ }
+ };
+
+// --- Search Functionality ---
+
+ const handleSearch = (value) => {
+ if (activeTab === 'users') {
+ const filteredData = userListData.filter(item =>
+ Object.values(item).some(val =>
+ String(val).toLowerCase().includes(value.toLowerCase())
+ )
+ );
+ if (value === '') {
+ fetchUsers();
+ } else {
+ setUserListData(filteredData);
+ }
+ } else {
+ fetchAcls(value);
+ }
+ };
+
+
+ // --- User Table Column Definitions ---
+ const userColumns = [
+ {
+ title: t.USERNAME,
+ dataIndex: 'username',
+ key: 'username',
+ },
+ {
+ title: t.PASSWORD,
+ dataIndex: 'password',
+ key: 'password',
+ render: (text) => (
+
+ {showPassword ? text : '********'}
+ : }
+ onClick={() => setShowPassword(!showPassword)}
+ style={{ marginLeft: 8 }}
+ >
+ {showPassword ? t.HIDE : t.VIEW}
+
+
+ ),
+ },
+ {
+ title: t.USER_TYPE,
+ dataIndex: 'userType',
+ key: 'userType',
+ },
+ {
+ title: t.USER_STATUS,
+ dataIndex: 'userStatus',
+ key: 'userStatus',
+ render: (status) => (
+ {status}
+ ),
+ },
+ {
+ title: t.OPERATION,
+ key: 'action',
+ render: (_, record) => (
+
+ } onClick={() => handleEditUser(record)}>{t.MODIFY}
+ handleDeleteUser(record.username)}
+ okText={t.YES}
+ cancelText={t.NO}
+ >
+ } danger>{t.DELETE}
+
+
+ ),
+ },
+ ];
+
+// --- ACL Permission Table Column Definitions ---
+ const aclColumns = [
+ {
+ title: t.USERNAME_SUBJECT,
+ dataIndex: 'subject',
+ key: 'subject',
+ },
+ {
+ title: t.POLICY_TYPE,
+ dataIndex: 'policyType',
+ key: 'policyType',
+ },
+ {
+ title: t.RESOURCE_NAME,
+ dataIndex: 'resource',
+ key: 'resource',
+ },
+ {
+ title: t.OPERATION_TYPE,
+ dataIndex: 'actions',
+ key: 'actions',
+ render: (text) => text ? text.split(', ').map((action, index) => (
+ {action}
+ )) : null,
+ },
+ {
+ title: t.SOURCE_IP,
+ dataIndex: 'sourceIps',
+ key: 'sourceIps',
+ },
+ {
+ title: t.DECISION,
+ dataIndex: 'decision',
+ key: 'decision',
+ render: (text) => (
+ {text}
+ ),
+ },
+ {
+ title: t.OPERATION,
+ key: 'action',
+ render: (_, record) => (
+
+ } onClick={() => handleEditAcl(record)}>{t.MODIFY}
+ handleDeleteAcl(record.subject, record.resource)}
+ okText={t.YES}
+ cancelText={t.NO}
+ >
+ } danger>{t.DELETE}
+
+
+ ),
+ },
+ ];
+
+return (
+ <>
+ {msgContextHolder}
+
+
{t.ACL_MANAGEMENT}
+
+
+
+
+
+
+
+ {activeTab === 'users' ? t.ADD_USER : t.ADD_ACL_PERMISSION}
+
+
+
+
+ {activeTab === 'users' && (
+
+ )}
+
+ {activeTab === 'acls' && (
+
+ )}
+
+ {/* User Management Modal */}
+
setIsUserModalVisible(false)}
+ confirmLoading={loading}
+ footer={[
+ setIsUserModalVisible(false)}>
+ {t.CANCEL}
+ ,
+
+ {t.CONFIRM}
+ ,
+ ]}
+ >
+
+
+
+
+ (visible ? : )}
+ />
+
+
+
+ Super
+ Normal
+
+
+
+
+ enable
+ disable
+
+
+
+
+
+ {/* ACL Permission Management Modal */}
+
setIsAclModalVisible(false)}
+ confirmLoading={loading}
+ >
+
+
+
+
+
+
+ Custom
+ Default
+
+
+
+
+ {isUpdate ? (
+
+ ) : (
+
+ )}
+
+
+
+
+ All
+ Pub
+ Sub
+ Create
+ Update
+ Delete
+ Get
+ List
+
+
+
+
+ 192.168.1.1
+ 0.0.0.0
+ 127.0.0.1
+
+
+
+
+ Allow
+ Deny
+
+
+
+
+
+ >
+);}
+
+export default Acl;
diff --git a/frontend-new/src/pages/Cluster/cluster.jsx b/frontend-new/src/pages/Cluster/cluster.jsx
new file mode 100644
index 0000000..26d2c24
--- /dev/null
+++ b/frontend-new/src/pages/Cluster/cluster.jsx
@@ -0,0 +1,303 @@
+/*
+ * 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, {useCallback, useEffect, useState} from 'react';
+import {Button, Modal, notification, Select, Spin, Table} from 'antd';
+import {useLanguage} from "../../i18n/LanguageContext";
+import {remoteApi, tools} from "../../api/remoteApi/remoteApi"; // 确保路径正确
+
+const {Option} = Select;
+
+const Cluster = () => {
+ const {t} = useLanguage();
+
+ const [loading, setLoading] = useState(false);
+ const [clusterNames, setClusterNames] = useState([]);
+ const [selectedCluster, setSelectedCluster] = useState('');
+ const [instances, setInstances] = useState([]);
+ const [allBrokersData, setAllBrokersData] = useState({});
+
+ const [detailModalVisible, setDetailModalVisible] = useState(false);
+ const [configModalVisible, setConfigModalVisible] = useState(false);
+ const [currentDetail, setCurrentDetail] = useState({});
+ const [currentConfig, setCurrentConfig] = useState({});
+ const [currentBrokerName, setCurrentBrokerName] = useState('');
+ const [currentIndex, setCurrentIndex] = useState(null); // 对应 brokerId
+ const [currentBrokerAddress, setCurrentBrokerAddress] = useState('');
+ const [api, contextHolder] = notification.useNotification();
+
+ const switchCluster = useCallback((clusterName) => {
+ if (allBrokersData[clusterName]) {
+ setInstances(allBrokersData[clusterName]);
+ } else {
+ setInstances([]);
+ }
+ }, [allBrokersData]);
+
+ const handleChangeCluster = (value) => {
+ setSelectedCluster(value);
+ switchCluster(value);
+ };
+
+ useEffect(() => {
+ setLoading(true);
+ remoteApi.queryClusterList((resp) => {
+ setLoading(false);
+ if (resp.status === 0) {
+ const {clusterInfo, brokerServer} = resp.data;
+ const {clusterAddrTable, brokerAddrTable} = clusterInfo;
+
+ const generatedBrokers = tools.generateBrokerMap(brokerServer, clusterAddrTable, brokerAddrTable);
+ setAllBrokersData(generatedBrokers);
+
+ const names = Object.keys(clusterAddrTable);
+ setClusterNames(names);
+
+ if (names.length > 0) {
+ const defaultCluster = names[0];
+ setSelectedCluster(defaultCluster);
+ if (generatedBrokers[defaultCluster]) {
+ setInstances(generatedBrokers[defaultCluster]);
+ } else {
+ setInstances([]);
+ }
+ }
+
+ } else {
+ api.error({message: resp.errMsg || t.QUERY_CLUSTER_LIST_FAILED, duration: 2});
+ }
+ });
+ }, []);
+
+ const showDetail = (brokerName, brokerId, record) => { // 传入 record 整个对象,方便直接显示
+ setCurrentBrokerName(brokerName);
+ setCurrentIndex(brokerId);
+ setCurrentDetail(record); // 直接使用 record 作为详情
+ setDetailModalVisible(true);
+ };
+
+ const showConfig = (brokerAddress, brokerName, brokerId) => { // 保持一致,传入 brokerId
+ setCurrentBrokerName(brokerName);
+ setCurrentIndex(brokerId);
+ setCurrentBrokerAddress(brokerAddress);
+
+ setLoading(true);
+ remoteApi.queryBrokerConfig(brokerAddress, (resp) => {
+ setLoading(false);
+ if (resp.status === 0) {
+ // ✨ 确保 resp.data 是一个对象,如果后端返回的不是对象,这里需要处理
+ if (typeof resp.data === 'object' && resp.data !== null) {
+ setCurrentConfig(resp.data);
+ setConfigModalVisible(true);
+ } else {
+ api.error({message: t.INVALID_CONFIG_DATA || 'Invalid config data received', duration: 2});
+ setCurrentConfig({}); // 清空配置,避免显示错误
+ }
+ } else {
+ api.error({message: resp.errMsg || t.QUERY_BROKER_CONFIG_FAILED, duration: 2});
+ }
+ });
+ };
+
+ const columns = [
+ {
+ title: t.SPLIT,
+ dataIndex: 'brokerName', // 直接使用 brokerId
+ key: 'split',
+ align: 'center'
+ },
+ {
+ title: t.NO,
+ dataIndex: 'brokerId', // 直接使用 brokerId
+ key: 'no',
+ align: 'center',
+ render: (brokerId) => `${brokerId}${brokerId === 0 ? `(${t.MASTER})` : `(${t.SLAVE})`}`,
+ },
+ {
+ title: t.ADDRESS,
+ dataIndex: 'address', // 确保 generateBrokerMap 返回的数据有 address 字段
+ key: 'address',
+ align: 'center',
+ },
+ {
+ title: t.VERSION,
+ dataIndex: 'brokerVersionDesc',
+ key: 'version',
+ align: 'center',
+ },
+ {
+ title: t.PRO_MSG_TPS,
+ dataIndex: 'putTps',
+ key: 'putTps',
+ align: 'center',
+ render: (text) => {
+ const tpsValue = text ? Number(String(text).split(' ')[0]) : 0; // 确保text是字符串
+ return tpsValue.toFixed(2);
+ },
+ },
+ {
+ title: t.CUS_MSG_TPS,
+ key: 'cusMsgTps',
+ align: 'center',
+ render: (_, record) => {
+ // 根据你提供的数据结构,这里可能是 getTransferredTps
+ const val = record.getTransferedTps?.trim() ? record.getTransferedTps : record.getTransferredTps;
+ const tpsValue = val ? Number(String(val).split(' ')[0]) : 0; // 确保val是字符串
+ return tpsValue.toFixed(2);
+ },
+ },
+ {
+ title: t.YESTERDAY_PRO_COUNT,
+ key: 'yesterdayProCount',
+ align: 'center',
+ render: (_, record) => {
+ const putTotalTodayMorning = parseFloat(record.msgPutTotalTodayMorning || 0);
+ const putTotalYesterdayMorning = parseFloat(record.msgPutTotalYesterdayMorning || 0);
+ return (putTotalTodayMorning - putTotalYesterdayMorning).toLocaleString();
+ }
+ },
+ {
+ title: t.YESTERDAY_CUS_COUNT,
+ key: 'yesterdayCusCount',
+ align: 'center',
+ render: (_, record) => {
+ const getTotalTodayMorning = parseFloat(record.msgGetTotalTodayMorning || 0);
+ const getTotalYesterdayMorning = parseFloat(record.msgGetTotalYesterdayMorning || 0);
+ return (getTotalTodayMorning - getTotalYesterdayMorning).toLocaleString();
+ }
+ },
+ {
+ title: t.TODAY_PRO_COUNT,
+ key: 'todayProCount',
+ align: 'center',
+ render: (_, record) => {
+ const putTotalTodayNow = parseFloat(record.msgPutTotalTodayNow || 0);
+ const putTotalTodayMorning = parseFloat(record.msgPutTotalTodayMorning || 0);
+ return (putTotalTodayNow - putTotalTodayMorning).toLocaleString();
+ }
+ },
+ {
+ title: t.TODAY_CUS_COUNT,
+ key: 'todayCusCount',
+ align: 'center',
+ render: (_, record) => {
+ const getTotalTodayNow = parseFloat(record.msgGetTotalTodayNow || 0);
+ const getTotalTodayMorning = parseFloat(record.msgGetTotalTodayMorning || 0);
+ return (getTotalTodayNow - getTotalTodayMorning).toLocaleString();
+ }
+ },
+ {
+ title: t.OPERATION,
+ key: 'operation',
+ align: 'center',
+ render: (_, record) => (
+ <>
+ showDetail(record.brokerName, record.brokerId, record)}
+ style={{marginRight: 8}}>
+ {t.STATUS}
+
+ {/* 传入 record.address */}
+ showConfig(record.address, record.brokerName, record.brokerId)}>
+ {t.CONFIG}
+
+ >
+ ),
+ },
+ ];
+
+ return (
+ <>
+ {contextHolder}
+
+
+
+ {t.CLUSTER}:
+
+ {clusterNames.map((name) => (
+
+ {name}
+
+ ))}
+
+
+
+
`${record.brokerName}-${record.brokerId}`}
+ pagination={false}
+ bordered
+ size="middle"
+ />
+
+ setDetailModalVisible(false)}
+ width={800}
+ bodyStyle={{maxHeight: '60vh', overflowY: 'auto'}}
+ >
+ ({key, value}))}
+ columns={[
+ {title: t.KEY || 'Key', dataIndex: 'key', key: 'key'},
+ {title: t.VALUE || 'Value', dataIndex: 'value', key: 'value'},
+ ]}
+ pagination={false}
+ size="small"
+ bordered
+ rowKey="key"
+ />
+
+
+ setConfigModalVisible(false)}
+ width={800}
+ bodyStyle={{maxHeight: '60vh', overflowY: 'auto'}}
+ >
+ ({key, value}))}
+ columns={[
+ {title: t.KEY || 'Key', dataIndex: 'key', key: 'key'},
+ {title: t.VALUE || 'Value', dataIndex: 'value', key: 'value'},
+ ]}
+ pagination={false}
+ size="small"
+ bordered
+ rowKey="key"
+ />
+
+
+
+ >
+
+ );
+};
+
+export default Cluster;
diff --git a/frontend-new/src/pages/Consumer/consumer.jsx b/frontend-new/src/pages/Consumer/consumer.jsx
new file mode 100644
index 0000000..d67efa8
--- /dev/null
+++ b/frontend-new/src/pages/Consumer/consumer.jsx
@@ -0,0 +1,480 @@
+/*
+ * 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, {useCallback, useEffect, useState} from 'react';
+import {Button, Checkbox, Input, message, notification, Spin, Table} from 'antd';
+import {useLanguage} from '../../i18n/LanguageContext';
+import {remoteApi} from '../../api/remoteApi/remoteApi';
+import ClientInfoModal from "../../components/consumer/ClientInfoModal";
+import ConsumerDetailModal from "../../components/consumer/ConsumerDetailModal";
+import ConsumerConfigModal from "../../components/consumer/ConsumerConfigModal";
+import DeleteConsumerModal from "../../components/consumer/DeleteConsumerModal";
+
+const ConsumerGroupList = () => {
+ const {t} = useLanguage();
+ const [filterStr, setFilterStr] = useState('');
+ const [filterNormal, setFilterNormal] = useState(true);
+ const [filterFIFO, setFilterFIFO] = useState(false);
+ const [filterSystem, setFilterSystem] = useState(false);
+ const [rmqVersion, setRmqVersion] = useState(true);
+ const [writeOperationEnabled, setWriteOperationEnabled] = useState(true);
+ const [intervalProcessSwitch, setIntervalProcessSwitch] = useState(false);
+ const [loading, setLoading] = useState(false);
+ const [consumerGroupShowList, setConsumerGroupShowList] = useState([]);
+ const [allConsumerGroupList, setAllConsumerGroupList] = useState([]);
+ const [selectedGroup, setSelectedGroup] = useState(null);
+ const [selectedAddress, setSelectedAddress] = useState(null);
+ const [showClientInfo, setShowClientInfo] = useState(false);
+ const [showConsumeDetail, setShowConsumeDetail] = useState(false);
+ const [showConfig, setShowConfig] = useState(false);
+ const [isAddConfig, setIsAddConfig] = useState(false);
+ const [showDeleteModal, setShowDeleteModal] = useState(false);
+ const [messageApi, msgContextHolder] = message.useMessage();
+ const [notificationApi,notificationContextHolder] = notification.useNotification();
+
+ const [paginationConf, setPaginationConf] = useState({
+ current: 1,
+ pageSize: 10,
+ total: 0,
+ });
+
+ const [sortConfig, setSortConfig] = useState({
+ sortKey: null,
+ sortOrder: 1,
+ });
+
+ const loadConsumerGroups = useCallback(async (currentPage) => {
+ setLoading(true);
+ try {
+ const response = await remoteApi.queryConsumerGroupList(false);
+ if (response.status === 0) {
+ setAllConsumerGroupList(response.data);
+ if(currentPage!=null){
+ filterList(currentPage, response.data);
+ }else{
+ filterList(1, response.data);
+ }
+ } else {
+ messageApi.error({title: t.ERROR, content: response.errMsg});
+ }
+ } catch (error) {
+ messageApi.error({title: t.ERROR, content: t.FAILED_TO_FETCH_DATA});
+ console.error("Error loading consumer groups:", error);
+ } finally {
+ setLoading(false);
+ }
+ }, [t]);
+
+ const filterByType = (str, type, version) => {
+ if (filterSystem && type === "SYSTEM") return true;
+ if (filterNormal && (type === "NORMAL" || (!version && type === "FIFO"))) return true;
+ if (filterFIFO && type === "FIFO") return true;
+ return false;
+ };
+
+ const filterList = useCallback((currentPage, data) => {
+ // 排序处理
+ let sortedData = [...data];
+ if (sortConfig.sortKey) {
+ sortedData.sort((a, b) => {
+ const aValue = a[sortConfig.sortKey];
+ const bValue = b[sortConfig.sortKey];
+ if (typeof aValue === 'string') {
+ return sortConfig.sortOrder * aValue.localeCompare(bValue);
+ }
+ return sortConfig.sortOrder * (aValue > bValue ? 1 : -1);
+ });
+ }
+
+ // 过滤处理
+ const lowExceptStr = filterStr.toLowerCase();
+ const canShowList = sortedData.filter(element =>
+ filterByType(element.group, element.subGroupType, rmqVersion) &&
+ element.group.toLowerCase().includes(lowExceptStr)
+ );
+
+ // 更新分页和显示列表
+ const perPage = paginationConf.pageSize;
+ const from = (currentPage - 1) * perPage;
+ const to = from + perPage;
+
+ setPaginationConf(prev => ({
+ ...prev,
+ current: currentPage,
+ total: canShowList.length,
+ }));
+ setConsumerGroupShowList(canShowList.slice(from, to));
+ }, [filterStr, filterNormal, filterSystem, filterFIFO, rmqVersion, sortConfig, paginationConf.pageSize]);
+
+
+ const doSort = useCallback(() => {
+ const sortedList = [...allConsumerGroupList];
+
+ if (sortConfig.sortKey === 'diffTotal') {
+ sortedList.sort((a, b) => {
+ return (a.diffTotal > b.diffTotal) ? sortConfig.sortOrder :
+ ((b.diffTotal > a.diffTotal) ? -sortConfig.sortOrder : 0);
+ });
+ }
+ if (sortConfig.sortKey === 'group') {
+ sortedList.sort((a, b) => {
+ return (a.group > b.group) ? sortConfig.sortOrder :
+ ((b.group > a.group) ? -sortConfig.sortOrder : 0);
+ });
+ }
+ if (sortConfig.sortKey === 'count') {
+ sortedList.sort((a, b) => {
+ return (a.count > b.count) ? sortConfig.sortOrder :
+ ((b.count > a.count) ? -sortConfig.sortOrder : 0);
+ });
+ }
+ if (sortConfig.sortKey === 'consumeTps') {
+ sortedList.sort((a, b) => {
+ return (a.consumeTps > b.consumeTps) ? sortConfig.sortOrder :
+ ((b.consumeTps > a.consumeTps) ? -sortConfig.sortOrder : 0);
+ });
+ }
+
+ setAllConsumerGroupList(sortedList);
+ filterList(paginationConf.current, sortedList);
+ }, [sortConfig, allConsumerGroupList, paginationConf.current]);
+
+ useEffect(() => {
+ loadConsumerGroups();
+ }, [loadConsumerGroups]);
+
+ useEffect(() => {
+ let intervalId;
+ if (intervalProcessSwitch) {
+ intervalId = setInterval(loadConsumerGroups, 10000);
+ }
+ return () => clearInterval(intervalId);
+ }, [intervalProcessSwitch, loadConsumerGroups]);
+
+
+ useEffect(() => {
+ filterList(paginationConf.current, allConsumerGroupList);
+ }, [allConsumerGroupList, filterStr, filterNormal, filterSystem, filterFIFO, sortConfig, filterList, paginationConf.current]);
+
+ const handleFilterInputChange = (value) => {
+ setFilterStr(value);
+ setPaginationConf(prev => ({...prev, current: 1}));
+ };
+
+ const handleTypeFilterChange = (filterType, checked) => {
+ switch (filterType) {
+ case 'normal':
+ setFilterNormal(checked);
+ break;
+ case 'fifo':
+ setFilterFIFO(checked);
+ break;
+ case 'system':
+ setFilterSystem(checked);
+ break;
+ default:
+ break;
+ }
+ setPaginationConf(prev => ({...prev, current: 1}));
+ };
+
+ const handleRefreshConsumerData = async () => {
+ setLoading(true);
+ const refreshResult = await remoteApi.refreshAllConsumerGroup();
+ setLoading(false);
+
+ if (refreshResult && refreshResult.status === 0) {
+ notificationApi.success({message: t.REFRESH_SUCCESS, duration: 2});
+ loadConsumerGroups();
+ } else if (refreshResult && refreshResult.errMsg) {
+ notificationApi.error({message: t.REFRESH_FAILED + ": " + refreshResult.errMsg, duration: 2});
+ } else {
+ notificationApi.error({message: t.REFRESH_FAILED, duration: 2});
+ }
+ };
+
+ const handleOpenAddDialog = () => {
+ setIsAddConfig(true)
+ setShowConfig(true);
+ };
+
+ // 修改操作按钮的点击处理函数
+ const handleClient = (group, address) => {
+ setSelectedGroup(group);
+ setSelectedAddress(address);
+ setShowClientInfo(true);
+ };
+
+ const handleDetail = (group, address) => {
+ setSelectedGroup(group);
+ setSelectedAddress(address);
+ setShowConsumeDetail(true);
+ };
+
+ const handleUpdateConfigDialog = (group) => {
+ setSelectedGroup(group);
+ setShowConfig(true);
+ };
+
+
+ const handleDelete = (group) => {
+ setSelectedGroup(group);
+ setShowDeleteModal(true);
+ };
+
+ const handleRefreshConsumerGroup = async (group) => {
+ setLoading(true);
+ const response = await remoteApi.refreshConsumerGroup(group);
+ setLoading(false);
+ if (response.status === 0) {
+ messageApi.success({content: `${group} ${t.REFRESHED}`});
+ loadConsumerGroups(paginationConf.current);
+ } else {
+ messageApi.error({title: t.ERROR, content: response.errMsg});
+ }
+ };
+
+
+ const handleSort = (sortKey) => {
+ setSortConfig(prev => ({
+ sortKey,
+ sortOrder: prev.sortKey === sortKey ? -prev.sortOrder : 1,
+ }));
+ setPaginationConf(prev => ({...prev, current: 1}));
+ };
+
+ const columns = [
+ {
+ title: handleSort('group')}>{t.SUBSCRIPTION_GROUP} ,
+ dataIndex: 'group',
+ key: 'group',
+ align: 'center',
+ render: (text) => {
+ const sysFlag = text.startsWith('%SYS%');
+ return (
+
+ {sysFlag ? text.substring(5) : text}
+
+ );
+ },
+ },
+ {
+ title: handleSort('count')}>{t.QUANTITY} ,
+ dataIndex: 'count',
+ key: 'count',
+ align: 'center',
+ },
+ {
+ title: t.VERSION,
+ dataIndex: 'version',
+ key: 'version',
+ align: 'center',
+ },
+ {
+ title: t.TYPE,
+ dataIndex: 'consumeType',
+ key: 'consumeType',
+ align: 'center',
+ },
+ {
+ title: t.MODE,
+ dataIndex: 'messageModel',
+ key: 'messageModel',
+ align: 'center',
+ },
+ {
+ title: handleSort('consumeTps')}>TPS ,
+ dataIndex: 'consumeTps',
+ key: 'consumeTps',
+ align: 'center',
+ },
+ {
+ title: handleSort('diffTotal')}>{t.DELAY} ,
+ dataIndex: 'diffTotal',
+ key: 'diffTotal',
+ align: 'center',
+ },
+ {
+ title: t.UPDATE_TIME,
+ dataIndex: 'updateTime',
+ key: 'updateTime',
+ align: 'center',
+ },
+ {
+ title: t.OPERATION,
+ key: 'operation',
+ align: 'left',
+ render: (_, record) => {
+ const sysFlag = record.group.startsWith('%SYS%');
+ return (
+ <>
+ handleClient(record.group, record.address)}
+ >
+ {t.CLIENT}
+
+ handleDetail(record.group, record.address)}
+ >
+ {t.CONSUME_DETAIL}
+
+ handleUpdateConfigDialog(record.group)}
+ >
+ {t.CONFIG}
+
+ handleRefreshConsumerGroup(record.group)}
+ >
+ {t.REFRESH}
+
+ {!sysFlag && writeOperationEnabled && (
+ handleDelete(record.group)}
+ >
+ {t.DELETE}
+
+ )}
+ >
+ );
+ },
+ },
+ ];
+
+ const handleTableChange = (pagination) => {
+ setPaginationConf(prev => ({
+ ...prev,
+ current: pagination.current,
+ pageSize: pagination.pageSize
+ }));
+ filterList(pagination.current, allConsumerGroupList);
+ };
+
+ const closeConfigModal = () =>{
+ setShowConfig(false);
+ setIsAddConfig(false);
+ }
+
+ return (
+ <>
+ {msgContextHolder}
+ {notificationContextHolder}
+
+
+
+
+
+ {t.SUBSCRIPTION_GROUP}:
+ handleFilterInputChange(e.target.value)}
+ />
+
+
handleTypeFilterChange('normal', e.target.checked)}>
+ {t.NORMAL}
+
+ {rmqVersion && (
+
handleTypeFilterChange('fifo', e.target.checked)}>
+ {t.FIFO}
+
+ )}
+
handleTypeFilterChange('system', e.target.checked)}>
+ {t.SYSTEM}
+
+ {writeOperationEnabled && (
+
+ {t.ADD} / {t.UPDATE}
+
+ )}
+
+ {t.REFRESH}
+
+ {/*
setIntervalProcessSwitch(checked)}*/}
+ {/* checkedChildren={t.AUTO_REFRESH}*/}
+ {/* unCheckedChildren={t.AUTO_REFRESH}*/}
+ {/*/>*/}
+
+
+
+
+
+
+
setShowClientInfo(false)}
+ />
+
+ setShowConsumeDetail(false)}
+ />
+
+
+
+ setShowDeleteModal(false)}
+ onSuccess={loadConsumerGroups}
+ />
+
+ >
+ );
+};
+
+export default ConsumerGroupList;
diff --git a/frontend-new/src/pages/Dashboard/DashboardPage.jsx b/frontend-new/src/pages/Dashboard/DashboardPage.jsx
new file mode 100644
index 0000000..8161a5d
--- /dev/null
+++ b/frontend-new/src/pages/Dashboard/DashboardPage.jsx
@@ -0,0 +1,455 @@
+/*
+ * 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, {useCallback, useEffect, useRef, useState} from 'react';
+import {Card, Col, DatePicker, message, notification, Row, Select, Spin, Table} from 'antd';
+import * as echarts from 'echarts';
+import moment from 'moment';
+import {useLanguage} from '../../i18n/LanguageContext';
+import {remoteApi, tools} from '../../api/remoteApi/remoteApi';
+
+const {Option} = Select;
+
+const DashboardPage = () => {
+ const {t} = useLanguage();
+ const barChartRef = useRef(null);
+ const lineChartRef = useRef(null);
+ const topicBarChartRef = useRef(null);
+ const topicLineChartRef = useRef(null);
+
+ const [loading, setLoading] = useState(false);
+ const [date, setDate] = useState(moment());
+ const [topicNames, setTopicNames] = useState([]);
+ const [selectedTopic, setSelectedTopic] = useState(null);
+ const [brokerTableData, setBrokerTableData] = useState([]);
+
+
+ const barChartInstance = useRef(null);
+ const lineChartInstance = useRef(null);
+ const topicBarChartInstance = useRef(null);
+ const topicLineChartInstance = useRef(null);
+
+ const [messageApi, msgContextHolder] = message.useMessage();
+ const [notificationApi, notificationContextHolder] = notification.useNotification();
+
+ const initChart = useCallback((chartRef, titleText, isLine = false) => {
+ if (chartRef.current) {
+ const chart = echarts.init(chartRef.current);
+ let option = {
+ title: {text: titleText},
+ tooltip: {},
+ legend: {data: ['TotalMsg']},
+ axisPointer: {type: 'shadow'},
+ xAxis: {
+ type: 'category',
+ data: [],
+ axisLabel: {
+ inside: false,
+ color: '#000000',
+ rotate: 0,
+ interval: 0
+ },
+ axisTick: {show: true},
+ axisLine: {show: true},
+ z: 10
+ },
+ yAxis: {
+ type: 'value',
+ boundaryGap: [0, '100%'],
+ axisLabel: {formatter: (value) => value.toFixed(2)},
+ splitLine: {show: true}
+ },
+ series: [{name: 'TotalMsg', type: 'bar', data: []}]
+ };
+
+ if (isLine) {
+ option = {
+ title: {text: titleText},
+ toolbox: {
+ feature: {
+ dataZoom: {yAxisIndex: 'none'},
+ restore: {},
+ saveAsImage: {}
+ }
+ },
+ tooltip: {trigger: 'axis', axisPointer: {animation: false}},
+ yAxis: {
+ type: 'value',
+ boundaryGap: [0, '80%'],
+ axisLabel: {formatter: (value) => value.toFixed(2)},
+ splitLine: {show: true}
+ },
+ dataZoom: [{
+ type: 'inside', start: 90, end: 100
+ }, {
+ start: 0,
+ end: 10,
+ handleIcon: 'M10.7,11.9v-1.3H9.3v1.3c-4.9,0.3-8.8,4.4-8.8,9.4c0,5,3.9,9.1,8.8,9.4v1.3h1.3v-1.3c4.9-0.3,8.8-4.4,8.8-9.4C19.5,16.3,15.6,12.2,10.7,11.9z M13.3,24.4H6.7V23h6.6V24.4z M13.3,19.6H6.7v-1.4h6.6V19.6z',
+ handleSize: '80%',
+ handleStyle: {
+ color: '#fff',
+ shadowBlur: 3,
+ shadowColor: 'rgba(0, 0, 0, 0.6)',
+ shadowOffsetX: 2,
+ shadowOffsetY: 2
+ }
+ }],
+ legend: {data: [], top: 30},
+ xAxis: {type: 'time', boundaryGap: false, data: []},
+ series: []
+ };
+ }
+ chart.setOption(option);
+ return chart;
+ }
+ return null;
+ }, []);
+
+ useEffect(() => {
+ barChartInstance.current = initChart(barChartRef, t.BROKER + ' TOP 10');
+ lineChartInstance.current = initChart(lineChartRef, t.BROKER + ' 5min trend', true);
+ topicBarChartInstance.current = initChart(topicBarChartRef, t.TOPIC + ' TOP 10');
+ topicLineChartInstance.current = initChart(topicLineChartRef, t.TOPIC + ' 5min trend', true);
+
+ return () => {
+ barChartInstance.current?.dispose();
+ lineChartInstance.current?.dispose();
+ topicBarChartInstance.current?.dispose();
+ topicLineChartInstance.current?.dispose();
+ };
+ }, [t, initChart]);
+
+ const getBrokerBarChartOp = useCallback((xAxisData, data) => {
+ return {
+ xAxis: {data: xAxisData},
+ series: [{name: 'TotalMsg', data: data}]
+ };
+ }, []);
+
+ const getBrokerLineChartOp = useCallback((legend, data) => {
+ const series = [];
+ let xAxisData = [];
+ let isFirstSeries = true;
+
+ Object.entries(data).forEach(([key, values]) => {
+ const tpsValues = [];
+ values.forEach(tpsValue => {
+ const tpsArray = tpsValue.split(",");
+ if (isFirstSeries) {
+ xAxisData.push(moment(parseInt(tpsArray[0])).format("HH:mm:ss"));
+ }
+ tpsValues.push(parseFloat(tpsArray[1]));
+ });
+ isFirstSeries = false;
+ series.push({
+ name: key,
+ type: 'line',
+ smooth: true,
+ symbol: 'none',
+ sampling: 'average',
+ data: tpsValues
+ });
+ });
+
+ return {
+ legend: {data: legend},
+ color: ["#FF0000", "#00BFFF", "#FF00FF", "#1ce322", "#000000", '#EE7942'],
+ xAxis: {type: 'category', boundaryGap: false, data: xAxisData},
+ series: series
+ };
+ }, []);
+
+ const getTopicLineChartOp = useCallback((legend, data) => {
+ const series = [];
+ let xAxisData = [];
+ let isFirstSeries = true;
+
+ Object.entries(data).forEach(([key, values]) => {
+ const tpsValues = [];
+ values.forEach(tpsValue => {
+ const tpsArray = tpsValue.split(",");
+ if (isFirstSeries) {
+ xAxisData.push(moment(parseInt(tpsArray[0])).format("HH:mm:ss"));
+ }
+ tpsValues.push(parseFloat(tpsArray[2]));
+ });
+ isFirstSeries = false;
+ series.push({
+ name: key,
+ type: 'line',
+ smooth: true,
+ symbol: 'none',
+ sampling: 'average',
+ data: tpsValues
+ });
+ });
+
+ return {
+ legend: {data: legend},
+ xAxis: {type: 'category', boundaryGap: false, data: xAxisData},
+ series: series
+ };
+ }, []);
+
+ const queryLineData = useCallback(async () => {
+ const _date = date ? date.format("YYYY-MM-DD") : moment().format("YYYY-MM-DD");
+
+ lineChartInstance.current?.showLoading();
+ await remoteApi.queryBrokerHisData(_date, (resp) => {
+ lineChartInstance.current?.hideLoading();
+ if (resp.status === 0) {
+ const _data = {};
+ const _xAxisData = [];
+ Object.entries(resp.data).forEach(([address, values]) => {
+ _data[address] = values;
+ _xAxisData.push(address);
+ });
+ lineChartInstance.current?.setOption(getBrokerLineChartOp(_xAxisData, _data));
+ } else {
+ notificationApi.error({message: resp.errMsg || t.QUERY_BROKER_HISTORY_FAILED, duration: 2});
+ }
+ });
+
+ if (selectedTopic) {
+ topicLineChartInstance.current?.showLoading();
+ await remoteApi.queryTopicHisData(_date, selectedTopic, (resp) => {
+ topicLineChartInstance.current?.hideLoading();
+ if (resp.status === 0) {
+ const _data = {};
+ _data[selectedTopic] = resp.data;
+ topicLineChartInstance.current?.setOption(getTopicLineChartOp([selectedTopic], _data));
+ } else {
+ notificationApi.error({message: resp.errMsg || t.QUERY_TOPIC_HISTORY_FAILED, duration: 2});
+ }
+ });
+ }
+ }, [date, selectedTopic, getBrokerLineChartOp, getTopicLineChartOp, t]);
+
+ useEffect(() => {
+ setLoading(true);
+ barChartInstance.current?.showLoading();
+ remoteApi.queryClusterList((resp) => {
+ setLoading(false);
+ barChartInstance.current?.hideLoading();
+ if (resp.status === 0) {
+ const clusterAddrTable = resp.data.clusterInfo.clusterAddrTable;
+ const brokerAddrTable = resp.data.clusterInfo.brokerAddrTable; // Corrected to brokerAddrTable
+ const brokerDetail = resp.data.brokerServer;
+ const clusterMap = tools.generateBrokerMap(brokerDetail, clusterAddrTable, brokerAddrTable);
+
+ let brokerArray = [];
+ Object.values(clusterMap).forEach(brokersInCluster => {
+ brokerArray = brokerArray.concat(brokersInCluster);
+ });
+
+ // Update broker table data
+ setBrokerTableData(brokerArray.map(broker => ({
+ ...broker,
+ key: broker.brokerName // Ant Design Table needs a unique key
+ })));
+
+ brokerArray.sort((firstBroker, lastBroker) => {
+ const firstTotalMsg = parseFloat(firstBroker.msgGetTotalTodayNow || 0);
+ const lastTotalMsg = parseFloat(lastBroker.msgGetTotalTodayNow || 0);
+ return lastTotalMsg - firstTotalMsg;
+ });
+
+ const xAxisData = [];
+ const data = [];
+ brokerArray.slice(0, 10).forEach(broker => {
+ xAxisData.push(`${broker.brokerName}:${broker.index}`);
+ data.push(parseFloat(broker.msgGetTotalTodayNow || 0));
+ });
+ barChartInstance.current?.setOption(getBrokerBarChartOp(xAxisData, data));
+ } else {
+ notificationApi.error({message: resp.errMsg || t.QUERY_CLUSTER_LIST_FAILED, duration: 2});
+ }
+ });
+ }, [getBrokerBarChartOp, t]);
+
+ useEffect(() => {
+ topicBarChartInstance.current?.showLoading();
+ remoteApi.queryTopicCurrentData((resp) => {
+ topicBarChartInstance.current?.hideLoading();
+ if (resp.status === 0) {
+ const topicList = resp.data;
+ topicList.sort((first, last) => {
+ const firstTotalMsg = parseFloat(first.split(",")[1] || 0);
+ const lastTotalMsg = parseFloat(last.split(",")[1] || 0);
+ return lastTotalMsg - firstTotalMsg;
+ });
+
+ const xAxisData = [];
+ const data = [];
+ const names = [];
+
+ topicList.forEach((currentData) => {
+ const currentArray = currentData.split(",");
+ names.push(currentArray[0]);
+ });
+ setTopicNames(names);
+
+ if (names.length > 0 && selectedTopic === null) {
+ setSelectedTopic(names[0]);
+ }
+
+ topicList.slice(0, 10).forEach((currentData) => {
+ const currentArray = currentData.split(",");
+ xAxisData.push(currentArray[0]);
+ data.push(parseFloat(currentArray[1] || 0));
+ });
+
+ const option = {
+ xAxis: {
+ data: xAxisData,
+ axisLabel: {
+ inside: false,
+ color: '#000000',
+ rotate: 60,
+ interval: 0
+ },
+ },
+ series: [{name: 'TotalMsg', data: data}]
+ };
+ topicBarChartInstance.current?.setOption(option);
+ } else {
+ notificationApi.error({message: resp.errMsg || t.QUERY_TOPIC_CURRENT_FAILED, duration: 2});
+ }
+ });
+ }, [selectedTopic, t]);
+
+ useEffect(() => {
+ if (barChartInstance.current && lineChartInstance.current && topicBarChartInstance.current && topicLineChartInstance.current) {
+ queryLineData();
+ }
+ }, [date, selectedTopic, queryLineData]);
+
+ useEffect(() => {
+ const intervalId = setInterval(queryLineData, tools.dashboardRefreshTime);
+ return () => {
+ clearInterval(intervalId);
+ };
+ }, [queryLineData]);
+
+ const brokerColumns = [
+ {title: t.BROKER_NAME, dataIndex: 'brokerName', key: 'brokerName'},
+ {title: t.BROKER_ADDR, dataIndex: 'brokerAddress', key: 'brokerAddress'},
+ {
+ title: t.TOTAL_MSG_RECEIVED_TODAY,
+ dataIndex: 'msgGetTotalTodayNow',
+ key: 'msgGetTotalTodayNow',
+ render: (text) => parseFloat(text || 0).toLocaleString(),
+ sorter: (a, b) => parseFloat(a.msgGetTotalTodayNow || 0) - parseFloat(b.msgGetTotalTodayNow || 0),
+ },
+ {
+ title: t.TODAY_PRO_COUNT,
+ key: 'todayProCount',
+ render: (_, record) => parseFloat(record.msgPutTotalTodayMorning || 0).toLocaleString(), // Assuming msgPutTotalTodayMorning is 'today pro count'
+ },
+ {
+ title: t.YESTERDAY_PRO_COUNT,
+ key: 'yesterdayProCount',
+ // This calculation (today morning - yesterday morning) might not be correct for 'yesterday pro count'.
+ // It depends on what msgPutTotalTodayMorning and msgPutTotalYesterdayMorning truly represent.
+ // If they are cumulative totals up to morning, then the difference is not accurate for yesterday's count.
+ // You might need a specific 'msgPutTotalYesterdayNow' from the backend.
+ render: (_, record) => (parseFloat(record.msgPutTotalTodayMorning || 0) - parseFloat(record.msgPutTotalYesterdayMorning || 0)).toLocaleString(),
+ },
+ ];
+
+ return (
+ <>
+ {msgContextHolder}
+ {notificationContextHolder}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ option.children.toLowerCase().indexOf(input.toLowerCase()) >= 0
+ }
+ >
+ {topicNames.map(topic => (
+ {topic}
+ ))}
+
+
+
+
+
+
+
+
+ >
+
+ );
+};
+
+export default DashboardPage;
diff --git a/frontend-new/src/pages/DlqMessage/dlqmessage.jsx b/frontend-new/src/pages/DlqMessage/dlqmessage.jsx
new file mode 100644
index 0000000..2591656
--- /dev/null
+++ b/frontend-new/src/pages/DlqMessage/dlqmessage.jsx
@@ -0,0 +1,703 @@
+/*
+ * 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, {useCallback, useEffect, useState} from 'react';
+import {
+ Button,
+ Checkbox,
+ DatePicker,
+ Form,
+ Input,
+ Modal,
+ notification,
+ Select,
+ Spin,
+ Table,
+ Tabs,
+ Typography
+} from 'antd';
+import moment from 'moment';
+import {ExportOutlined, SearchOutlined, SendOutlined} from '@ant-design/icons';
+import DlqMessageDetailViewDialog from "../../components/DlqMessageDetailViewDialog"; // Ensure this path is correct
+import {useLanguage} from '../../i18n/LanguageContext'; // Ensure this path is correct
+import {remoteApi} from '../../api/remoteApi/remoteApi'; // Adjust the path to your remoteApi.js file
+
+const {TabPane} = Tabs;
+const {Option} = Select;
+const {Text, Paragraph} = Typography;
+
+const SYS_GROUP_TOPIC_PREFIX = "CID_RMQ_SYS_"; // Define this constant as in Angular
+const DLQ_GROUP_TOPIC_PREFIX = "%DLQ%"; // Define this constant
+
+const DlqMessageQueryPage = () => {
+ const {t} = useLanguage();
+ const [activeTab, setActiveTab] = useState('consumer');
+ const [form] = Form.useForm();
+ const [loading, setLoading] = useState(false);
+
+ // Consumer 查询状态
+ const [allConsumerGroupList, setAllConsumerGroupList] = useState([]);
+ const [selectedConsumerGroup, setSelectedConsumerGroup] = useState(null);
+ const [timepickerBegin, setTimepickerBegin] = useState(moment().subtract(3, 'hour')); // 默认三小时前
+ const [timepickerEnd, setTimepickerEnd] = useState(moment());
+ const [messageShowList, setMessageShowList] = useState([]);
+ const [paginationConf, setPaginationConf] = useState({
+ current: 1,
+ pageSize: 20, // Adjusted to 20 as per Angular code
+ total: 0,
+ });
+ const [checkedAll, setCheckedAll] = useState(false);
+ const [selectedMessageIds, setSelectedMessageIds] = useState(new Set()); // Stores msgId for selected messages
+ const [messageCheckedList, setMessageCheckedList] = useState([]); // Stores full message objects for checked items
+ const [taskId, setTaskId] = useState("");
+
+
+ // Message ID 查询状态
+ const [messageId, setMessageId] = useState('');
+ const [queryDlqMessageByMessageIdResult, setQueryDlqMessageByMessageIdResult] = useState([]);
+ const [modalApi, modalContextHolder] = Modal.useModal();
+ const [notificationApi, notificationContextHolder] = notification.useNotification();
+ // Fetch consumer group list on component mount
+ useEffect(() => {
+ const fetchConsumerGroups = async () => {
+ setLoading(true);
+ const resp = await remoteApi.queryConsumerGroupList();
+ if (resp.status === 0) {
+ const filteredGroups = resp.data
+ .filter(consumerGroup => !consumerGroup.group.startsWith(SYS_GROUP_TOPIC_PREFIX))
+ .map(consumerGroup => consumerGroup.group)
+ .sort();
+ setAllConsumerGroupList(filteredGroups);
+ } else {
+ notificationApi.error({message: t.ERROR, description: resp.errMsg});
+ }
+ setLoading(false);
+ };
+ fetchConsumerGroups();
+ }, [t]);
+
+ // Effect to manage batch buttons' disabled state
+ useEffect(() => {
+ const batchResendBtn = document.getElementById('batchResendBtn');
+ const batchExportBtn = document.getElementById('batchExportBtn');
+ if (selectedMessageIds.size > 0) {
+ batchResendBtn?.classList.remove('disabled');
+ batchExportBtn?.classList.remove('disabled');
+ } else {
+ batchResendBtn?.classList.add('disabled');
+ batchExportBtn?.classList.add('disabled');
+ }
+ }, [selectedMessageIds]);
+
+ const onChangeQueryCondition = useCallback(() => {
+ // console.log("查询条件改变");
+ setTaskId(""); // Reset taskId when query conditions change
+ setPaginationConf(prev => ({...prev, currentPage: 1, totalItems: 0}));
+ }, []);
+
+ const queryDlqMessageByConsumerGroup = useCallback(async (page = paginationConf.current, pageSize = paginationConf.pageSize) => {
+ if (!selectedConsumerGroup) {
+ notificationApi.warning({
+ message: t.WARNING,
+ description: t.PLEASE_SELECT_CONSUMER_GROUP,
+ });
+ return;
+ }
+ if (moment(timepickerEnd).valueOf() < moment(timepickerBegin).valueOf()) {
+ notificationApi.error({message: t.END_TIME_LATER_THAN_BEGIN_TIME, delay: 2000});
+ return;
+ }
+
+ setLoading(true);
+ // console.log("根据消费者组查询DLQ消息:", { selectedConsumerGroup, timepickerBegin, timepickerEnd, page, pageSize, taskId });
+ try {
+ const resp = await remoteApi.queryDlqMessageByConsumerGroup(
+ selectedConsumerGroup,
+ moment(timepickerBegin).valueOf(),
+ moment(timepickerEnd).valueOf(),
+ page,
+ pageSize,
+ taskId
+ );
+
+ if (resp.status === 0) {
+ const fetchedMessages = resp.data.page.content.map(msg => ({...msg, checked: false}));
+ setMessageShowList(fetchedMessages);
+ if (fetchedMessages.length === 0) {
+ notificationApi.info({
+ message: t.NO_RESULT,
+ description: t.NO_MATCH_RESULT,
+ });
+ }
+ setPaginationConf(prev => ({
+ ...prev,
+ current: resp.data.page.number + 1,
+ pageSize: pageSize,
+ total: resp.data.page.totalElements,
+ }));
+ setTaskId(resp.data.taskId);
+ setSelectedMessageIds(new Set()); // Reset选中项
+ setCheckedAll(false); // Reset全选状态
+ setMessageCheckedList([]); // Clear checked list
+ } else {
+ notificationApi.error({
+ message: t.ERROR,
+ description: resp.errMsg,
+ });
+ }
+ } catch (error) {
+ notificationApi.error({
+ message: t.ERROR,
+ description: t.QUERY_FAILED,
+ });
+ console.error("查询失败:", error);
+ } finally {
+ setLoading(false);
+ }
+ }, [selectedConsumerGroup, timepickerBegin, timepickerEnd, paginationConf.current, paginationConf.pageSize, taskId, t]);
+
+ const queryDlqMessageByMessageId = useCallback(async () => {
+ if (!messageId || !selectedConsumerGroup) {
+ notificationApi.warning({
+ message: t.WARNING,
+ description: t.MESSAGE_ID_AND_CONSUMER_GROUP_REQUIRED,
+ });
+ return;
+ }
+ setLoading(true);
+ // console.log("根据Message ID查询DLQ消息:", { msgId: messageId, consumerGroup: selectedConsumerGroup });
+ try {
+ const resp = await remoteApi.viewMessage(messageId, DLQ_GROUP_TOPIC_PREFIX + selectedConsumerGroup);
+ if (resp.status === 0) {
+ setQueryDlqMessageByMessageIdResult(resp.data ? [resp.data] : []);
+ if (!resp.data) {
+ notificationApi.info({
+ message: t.NO_RESULT,
+ description: t.NO_MATCH_RESULT,
+ });
+ }
+ } else {
+ notificationApi.error({
+ message: t.ERROR,
+ description: resp.errMsg,
+ });
+ }
+ } catch (error) {
+ notificationApi.error({
+ message: t.ERROR,
+ description: t.QUERY_FAILED,
+ });
+ console.error("查询失败:", error);
+ } finally {
+ setLoading(false);
+ }
+ }, [messageId, selectedConsumerGroup, t]);
+
+ const queryDlqMessageDetail = useCallback(async (msgId, consumerGroup) => {
+ setLoading(true);
+ // console.log(`查询DLQ消息详情: ${msgId}, 消费者组: ${consumerGroup}`);
+ try {
+ const resp = await remoteApi.viewMessage(msgId, DLQ_GROUP_TOPIC_PREFIX + consumerGroup);
+ if (resp.status === 0) {
+ modalApi.info({
+ title: t.MESSAGE_DETAIL,
+ width: 800,
+ content: (
+
+ ),
+ onOk: () => {
+ },
+ okText: t.CLOSE,
+ });
+ } else {
+ notificationApi.error({
+ message: t.ERROR,
+ description: resp.errMsg,
+ });
+ }
+ } catch (error) {
+ notificationApi.error({
+ message: t.ERROR,
+ description: t.QUERY_FAILED,
+ });
+ console.error("查询失败:", error);
+ } finally {
+ setLoading(false);
+ }
+ }, [t]);
+
+ const resendDlqMessage = useCallback(async (messageView, consumerGroup) => {
+ setLoading(true);
+ const topic = messageView.properties.RETRY_TOPIC;
+ const msgId = messageView.properties.ORIGIN_MESSAGE_ID;
+ // console.log(`重发DLQ消息: MsgId=${msgId}, Topic=${topic}, 消费者组=${consumerGroup}`);
+ try {
+ const resp = await remoteApi.resendDlqMessage(msgId, consumerGroup, topic);
+ if (resp.status === 0) {
+ notificationApi.success({
+ message: t.SUCCESS,
+ description: t.RESEND_SUCCESS,
+ });
+ modalApi.info({
+ title: t.RESULT,
+ content: resp.data,
+ });
+ // Refresh list
+ queryDlqMessageByConsumerGroup(paginationConf.current, paginationConf.pageSize);
+ } else {
+ notificationApi.error({
+ message: t.ERROR,
+ description: resp.errMsg,
+ });
+ modalApi.error({
+ title: t.RESULT,
+ content: resp.errMsg,
+ });
+ }
+ } catch (error) {
+ notificationApi.error({
+ message: t.ERROR,
+ description: t.RESEND_FAILED,
+ });
+ console.error("重发失败:", error);
+ } finally {
+ setLoading(false);
+ }
+ }, [paginationConf.current, paginationConf.pageSize, queryDlqMessageByConsumerGroup, t]);
+
+ const exportDlqMessage = useCallback(async (msgId, consumerGroup) => {
+ setLoading(true);
+ // console.log(`导出DLQ消息: MsgId=${msgId}, 消费者组=${consumerGroup}`);
+ try {
+ const resp = await remoteApi.exportDlqMessage(msgId, consumerGroup);
+ if (resp.status === 0) {
+ notificationApi.success({
+ message: t.SUCCESS,
+ description: t.EXPORT_SUCCESS,
+ });
+ // The actual file download is handled within remoteApi.js
+ } else {
+ notificationApi.error({
+ message: t.ERROR,
+ description: resp.errMsg,
+ });
+ }
+ } catch (error) {
+ notificationApi.error({
+ message: t.ERROR,
+ description: t.EXPORT_FAILED,
+ });
+ console.error("导出失败:", error);
+ } finally {
+ setLoading(false);
+ }
+ }, [t]);
+
+ const batchResendDlqMessage = useCallback(async () => {
+ if (selectedMessageIds.size === 0) {
+ notificationApi.warning({
+ message: t.WARNING,
+ description: t.PLEASE_SELECT_MESSAGE_TO_RESEND,
+ });
+ return;
+ }
+ setLoading(true);
+ const messagesToResend = messageCheckedList.map(message => ({
+ topic: message.properties.RETRY_TOPIC,
+ msgId: message.properties.ORIGIN_MESSAGE_ID,
+ consumerGroup: selectedConsumerGroup,
+ }));
+ // console.log(`批量重发DLQ消息到 ${selectedConsumerGroup}:`, messagesToResend);
+ try {
+ const resp = await remoteApi.batchResendDlqMessage(messagesToResend);
+ if (resp.status === 0) {
+ notificationApi.success({
+ message: t.SUCCESS,
+ description: t.BATCH_RESEND_SUCCESS,
+ });
+ modalApi.info({
+ title: t.RESULT,
+ content: resp.data,
+ });
+ // Refresh list and reset selected state
+ queryDlqMessageByConsumerGroup(paginationConf.current, paginationConf.pageSize);
+ setSelectedMessageIds(new Set());
+ setCheckedAll(false);
+ setMessageCheckedList([]);
+ } else {
+ notificationApi.error({
+ message: t.ERROR,
+ description: resp.errMsg,
+ });
+ modalApi.error({
+ title: t.RESULT,
+ content: resp.errMsg,
+ });
+ }
+ } catch (error) {
+ notificationApi.error({
+ message: t.ERROR,
+ description: t.BATCH_RESEND_FAILED,
+ });
+ console.error("批量重发失败:", error);
+ } finally {
+ setLoading(false);
+ }
+ }, [selectedMessageIds, messageCheckedList, selectedConsumerGroup, paginationConf.current, paginationConf.pageSize, queryDlqMessageByConsumerGroup, t]);
+
+ const batchExportDlqMessage = useCallback(async () => {
+ if (selectedMessageIds.size === 0) {
+ notificationApi.warning({
+ message: t.WARNING,
+ description: t.PLEASE_SELECT_MESSAGE_TO_EXPORT,
+ });
+ return;
+ }
+ setLoading(true);
+ const messagesToExport = messageCheckedList.map(message => ({
+ msgId: message.msgId,
+ consumerGroup: selectedConsumerGroup,
+ }));
+ // console.log(`批量导出DLQ消息从 ${selectedConsumerGroup}:`, messagesToExport);
+ try {
+ const resp = await remoteApi.batchExportDlqMessage(messagesToExport);
+ if (resp.status === 0) {
+ notificationApi.success({
+ message: t.SUCCESS,
+ description: t.BATCH_EXPORT_SUCCESS,
+ });
+ // The actual file download is handled within remoteApi.js
+ // Refresh list and reset selected state
+ queryDlqMessageByConsumerGroup(paginationConf.current, paginationConf.pageSize);
+ setSelectedMessageIds(new Set());
+ setCheckedAll(false);
+ setMessageCheckedList([]);
+ } else {
+ notificationApi.error({
+ message: t.ERROR,
+ description: resp.errMsg,
+ });
+ }
+ } catch (error) {
+ notificationApi.error({
+ message: t.ERROR,
+ description: t.BATCH_EXPORT_FAILED,
+ });
+ console.error("批量导出失败:", error);
+ } finally {
+ setLoading(false);
+ }
+ }, [selectedMessageIds, messageCheckedList, selectedConsumerGroup, paginationConf.current, paginationConf.pageSize, queryDlqMessageByConsumerGroup, t]);
+
+ const handleSelectAll = (e) => {
+ const checked = e.target.checked;
+ setCheckedAll(checked);
+ const newSelectedIds = new Set();
+ const newCheckedList = [];
+ const updatedList = messageShowList.map(item => {
+ if (checked) {
+ newSelectedIds.add(item.msgId);
+ newCheckedList.push(item);
+ }
+ return {...item, checked};
+ });
+ setMessageShowList(updatedList);
+ setSelectedMessageIds(newSelectedIds);
+ setMessageCheckedList(newCheckedList);
+ };
+
+ const handleSelectItem = (item, checked) => {
+ const newSelectedIds = new Set(selectedMessageIds);
+ const newCheckedList = [...messageCheckedList];
+
+ if (checked) {
+ newSelectedIds.add(item.msgId);
+ newCheckedList.push(item);
+ } else {
+ newSelectedIds.delete(item.msgId);
+ const index = newCheckedList.findIndex(msg => msg.msgId === item.msgId);
+ if (index > -1) {
+ newCheckedList.splice(index, 1);
+ }
+ }
+ setSelectedMessageIds(newSelectedIds);
+ setMessageCheckedList(newCheckedList);
+
+ // Update single item checked state in the displayed list
+ const updatedList = messageShowList.map(msg =>
+ msg.msgId === item.msgId ? {...msg, checked} : msg
+ );
+ setMessageShowList(updatedList);
+
+ // Check if all are selected
+ setCheckedAll(newSelectedIds.size === updatedList.length && updatedList.length > 0);
+ };
+
+
+ const consumerColumns = [
+ {
+ title: (
+
+ ),
+ dataIndex: 'checked',
+ key: 'checkbox',
+ align: 'center',
+ render: (checked, record) => (
+ handleSelectItem(record, e.target.checked)}
+ />
+ ),
+ },
+ {title: 'Message ID', dataIndex: 'msgId', key: 'msgId', align: 'center'},
+ {
+ title: 'Tag', dataIndex: ['properties', 'TAGS'], key: 'tags', align: 'center',
+ render: (tags) => tags || '-' // Display '-' if tags are null or undefined
+ },
+ {
+ title: 'Key', dataIndex: ['properties', 'KEYS'], key: 'keys', align: 'center',
+ render: (keys) => keys || '-' // Display '-' if keys are null or undefined
+ },
+ {
+ title: 'StoreTime',
+ dataIndex: 'storeTimestamp',
+ key: 'storeTimestamp',
+ align: 'center',
+ render: (text) => moment(text).format("YYYY-MM-DD HH:mm:ss"),
+ },
+ {
+ title: 'Operation',
+ key: 'operation',
+ align: 'center',
+ render: (_, record) => (
+ <>
+ queryDlqMessageDetail(record.msgId, selectedConsumerGroup)}>
+ {t.MESSAGE_DETAIL}
+
+ resendDlqMessage(record, selectedConsumerGroup)}>
+ {t.RESEND_MESSAGE}
+
+ exportDlqMessage(record.msgId, selectedConsumerGroup)}>
+ {t.EXPORT}
+
+ >
+ ),
+ },
+ ];
+
+ const messageIdColumns = [
+ {title: 'Message ID', dataIndex: 'msgId', key: 'msgId', align: 'center'},
+ {
+ title: 'Tag', dataIndex: ['properties', 'TAGS'], key: 'tags', align: 'center',
+ render: (tags) => tags || '-'
+ },
+ {
+ title: 'Key', dataIndex: ['properties', 'KEYS'], key: 'keys', align: 'center',
+ render: (keys) => keys || '-'
+ },
+ {
+ title: 'StoreTime',
+ dataIndex: 'storeTimestamp',
+ key: 'storeTimestamp',
+ align: 'center',
+ render: (text) => moment(text).format("YYYY-MM-DD HH:mm:ss"),
+ },
+ {
+ title: 'Operation',
+ key: 'operation',
+ align: 'center',
+ render: (_, record) => (
+ <>
+ queryDlqMessageDetail(record.msgId, selectedConsumerGroup)}>
+ {t.MESSAGE_DETAIL}
+
+ resendDlqMessage(record, selectedConsumerGroup)}>
+ {t.RESEND_MESSAGE}
+
+ exportDlqMessage(record.msgId, selectedConsumerGroup)}>
+ {t.EXPORT}
+
+ >
+ ),
+ },
+ ];
+
+ return (
+ <>
+ {notificationContextHolder}
+
+
+
+
+ {t.TOTAL_MESSAGES}
+
+
+ {
+ setSelectedConsumerGroup(value);
+ onChangeQueryCondition();
+ }}
+ filterOption={(input, option) =>
+ option.children.toLowerCase().indexOf(input.toLowerCase()) >= 0
+ }
+ >
+ {allConsumerGroupList.map(group => (
+ {group}
+ ))}
+
+
+
+ {
+ setTimepickerBegin(date);
+ onChangeQueryCondition();
+ }}
+ />
+
+
+ {
+ setTimepickerEnd(date);
+ onChangeQueryCondition();
+ }}
+ />
+
+
+ }
+ onClick={() => queryDlqMessageByConsumerGroup()}>
+ {t.SEARCH}
+
+
+
+ }
+ onClick={batchResendDlqMessage}
+ disabled={selectedMessageIds.size === 0}
+ >
+ {t.BATCH_RESEND}
+
+
+
+ }
+ onClick={batchExportDlqMessage}
+ disabled={selectedMessageIds.size === 0}
+ >
+ {t.BATCH_EXPORT}
+
+
+
+
queryDlqMessageByConsumerGroup(page, pageSize),
+ showSizeChanger: true, // Allow changing page size
+ pageSizeOptions: ['10', '20', '50', '100'], // Customizable page size options
+ }}
+ locale={{emptyText: t.NO_MATCH_RESULT}}
+ />
+
+
+
+
+ {t.MESSAGE_ID_CONSUMER_GROUP_HINT}
+
+
+
+
+ option.children.toLowerCase().indexOf(input.toLowerCase()) >= 0
+ }
+ >
+ {allConsumerGroupList.map(group => (
+ {group}
+ ))}
+
+
+
+ setMessageId(e.target.value)}
+ />
+
+
+ }
+ onClick={queryDlqMessageByMessageId}>
+ {t.SEARCH}
+
+
+
+
+
+
+ {modalContextHolder}
+
+
+
+ >
+
+ );
+};
+
+export default DlqMessageQueryPage;
diff --git a/frontend-new/src/pages/Login/login.jsx b/frontend-new/src/pages/Login/login.jsx
new file mode 100644
index 0000000..befb025
--- /dev/null
+++ b/frontend-new/src/pages/Login/login.jsx
@@ -0,0 +1,90 @@
+/*
+ * 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 from 'react';
+import { Form, Input, Button, message, Typography } from 'antd';
+import {remoteApi} from "../../api/remoteApi/remoteApi";
+
+const { Title } = Typography;
+
+const Login = () => {
+ const [form] = Form.useForm();
+ const [messageApi, msgContextHolder] = message.useMessage();
+
+ const onFinish = async (values) => {
+ const { username, password } = values;
+ remoteApi.login(username, password).then((res) => {
+ if (res.status === 0) {
+ messageApi.success('登录成功');
+ window.sessionStorage.setItem("username", res.data.loginUserName);
+ window.sessionStorage.setItem("userrole", res.data.loginUserRole);
+ window.location.href = '/';
+ } else {
+ messageApi.error(res.message || '登录失败,请检查用户名和密码');
+ }
+ })
+ };
+
+ return (
+ <>
+ {msgContextHolder}
+
+
+ WELCOME
+
+
+
+
+
+
+
+
+
+
+
+ 登录
+
+
+
+
+ >
+
+ );
+};
+
+export default Login;
diff --git a/frontend-new/src/pages/Message/message.jsx b/frontend-new/src/pages/Message/message.jsx
new file mode 100644
index 0000000..73d3baf
--- /dev/null
+++ b/frontend-new/src/pages/Message/message.jsx
@@ -0,0 +1,478 @@
+/*
+ * 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, {useCallback, useEffect, useState} from 'react';
+import {Button, DatePicker, Form, Input, notification, Select, Spin, Table, Tabs, Typography} from 'antd';
+import moment from 'moment';
+import {SearchOutlined} from '@ant-design/icons';
+import {useLanguage} from '../../i18n/LanguageContext';
+import MessageDetailViewDialog from "../../components/MessageDetailViewDialog"; // Keep this path
+import {remoteApi} from '../../api/remoteApi/remoteApi'; // Keep this path
+
+const {TabPane} = Tabs;
+const {Option} = Select;
+const {Text, Paragraph} = Typography;
+
+const MessageQueryPage = () => {
+ const {t} = useLanguage();
+ const [activeTab, setActiveTab] = useState('topic');
+ const [form] = Form.useForm();
+ const [loading, setLoading] = useState(false);
+
+ // Topic 查询状态
+ const [allTopicList, setAllTopicList] = useState([]);
+ const [selectedTopic, setSelectedTopic] = useState(null);
+ const [timepickerBegin, setTimepickerBegin] = useState(moment().subtract(1, 'hour')); // 默认一小时前
+ const [timepickerEnd, setTimepickerEnd] = useState(moment());
+ const [messageShowList, setMessageShowList] = useState([]);
+ const [paginationConf, setPaginationConf] = useState({
+ current: 1,
+ pageSize: 10,
+ total: 0,
+ });
+ const [taskId, setTaskId] = useState("");
+
+ // Message Key 查询状态
+ const [key, setKey] = useState('');
+ const [queryMessageByTopicAndKeyResult, setQueryMessageByTopicAndKeyResult] = useState([]);
+
+ // Message ID 查询状态
+ const [messageId, setMessageId] = useState('');
+
+ // State for Message Detail Dialog
+ const [isMessageDetailModalVisible, setIsMessageDetailModalVisible] = useState(false);
+ const [currentMessageIdForDetail, setCurrentMessageIdForDetail] = useState(null);
+ const [currentTopicForDetail, setCurrentTopicForDetail] = useState(null);
+ const [notificationApi, notificationContextHolder] = notification.useNotification();
+
+ const fetchAllTopics = useCallback(async () => {
+ setLoading(true);
+ try {
+ const resp = await remoteApi.queryTopic(false);
+ if (resp.status === 0) {
+ setAllTopicList(resp.data.topicList.sort());
+ } else {
+ notificationApi.error({
+ message: t.ERROR,
+ description: resp.errMsg || t.FETCH_TOPIC_FAILED,
+ });
+ }
+ } catch (error) {
+ notificationApi.error({
+ message: t.ERROR,
+ description: t.FETCH_TOPIC_FAILED,
+ });
+ console.error("Error fetching topic list:", error);
+ } finally {
+ setLoading(false);
+ }
+ }, [t]);
+
+ useEffect(() => {
+ fetchAllTopics();
+ }, [fetchAllTopics]);
+
+ const onChangeQueryCondition = () => {
+ setTaskId("");
+ setPaginationConf(prev => ({
+ ...prev,
+ current: 1,
+ total: 0,
+ }));
+ };
+
+ const queryMessagePageByTopic = async (page = paginationConf.current, pageSize = paginationConf.pageSize) => {
+ if (!selectedTopic) {
+ notificationApi.warning({
+ message: t.WARNING,
+ description: t.PLEASE_SELECT_TOPIC,
+ });
+ return;
+ }
+ if (timepickerEnd.valueOf() < timepickerBegin.valueOf()) {
+ notificationApi.error({message: t.ERROR, description: t.END_TIME_EARLIER_THAN_BEGIN_TIME});
+ return;
+ }
+
+ setLoading(true);
+ try {
+ const resp = await remoteApi.queryMessagePageByTopic(
+ selectedTopic,
+ timepickerBegin.valueOf(),
+ timepickerEnd.valueOf(),
+ page,
+ pageSize,
+ taskId
+ );
+
+ if (resp.status === 0) {
+ setMessageShowList(resp.data.page.content);
+ setPaginationConf(prev => ({
+ ...prev,
+ current: resp.data.page.number + 1,
+ total: resp.data.page.totalElements,
+ pageSize: pageSize,
+ }));
+ setTaskId(resp.data.taskId);
+
+ if (resp.data.page.content.length === 0) {
+ notificationApi.info({
+ message: t.NO_RESULT,
+ description: t.NO_MATCH_RESULT,
+ });
+ }
+ } else {
+ notificationApi.error({
+ message: t.ERROR,
+ description: resp.errMsg || t.QUERY_FAILED,
+ });
+ }
+ } catch (error) {
+ notificationApi.error({
+ message: t.ERROR,
+ description: t.QUERY_FAILED,
+ });
+ console.error("查询失败:", error);
+ } finally {
+ setLoading(false);
+ }
+ };
+
+ const queryMessageByTopicAndKey = async () => {
+ if (!selectedTopic || !key) {
+ notificationApi.warning({
+ message: t.WARNING,
+ description: t.TOPIC_AND_KEY_REQUIRED,
+ });
+ return;
+ }
+ setLoading(true);
+ try {
+ const resp = await remoteApi.queryMessageByTopicAndKey(selectedTopic, key);
+ if (resp.status === 0) {
+ setQueryMessageByTopicAndKeyResult(resp.data);
+ if (resp.data.length === 0) {
+ notificationApi.info({
+ message: t.NO_RESULT,
+ description: t.NO_MATCH_RESULT,
+ });
+ }
+ } else {
+ notificationApi.error({
+ message: t.ERROR,
+ description: resp.errMsg || t.QUERY_FAILED,
+ });
+ }
+ } catch (error) {
+ notificationApi.error({
+ message: t.ERROR,
+ description: t.QUERY_FAILED,
+ });
+ console.error("查询失败:", error);
+ } finally {
+ setLoading(false);
+ }
+ };
+
+ // Updated to open the dialog
+ const showMessageDetail = (msgIdToQuery, topicToQuery) => {
+ if (!msgIdToQuery) {
+ notificationApi.warning({
+ message: t.WARNING,
+ description: t.MESSAGE_ID_REQUIRED,
+ });
+ return;
+ }
+ setCurrentMessageIdForDetail(msgIdToQuery);
+ setCurrentTopicForDetail(topicToQuery);
+ setIsMessageDetailModalVisible(true);
+ };
+
+ const handleCloseMessageDetailModal = () => {
+ setIsMessageDetailModalVisible(false);
+ setCurrentMessageIdForDetail(null);
+ setCurrentTopicForDetail(null);
+ };
+
+ const handleResendMessage = async (messageView, consumerGroup) => {
+ setLoading(true); // Set loading for the main page as well, as the dialog itself can't control it
+ let topicToResend = messageView.topic;
+ let msgIdToResend = messageView.msgId;
+
+
+ if (topicToResend.startsWith('%DLQ%')) {
+ if (messageView.properties && messageView.properties.hasOwnProperty("RETRY_TOPIC")) {
+ topicToResend = messageView.properties.RETRY_TOPIC;
+ }
+ if (messageView.properties && messageView.properties.hasOwnProperty("ORIGIN_MESSAGE_ID")) {
+ msgIdToResend = messageView.properties.ORIGIN_MESSAGE_ID;
+ }
+ }
+
+ try {
+ const resp = await remoteApi.resendMessageDirectly(msgIdToResend, consumerGroup, topicToResend);
+ if (resp.status === 0) {
+ notificationApi.success({
+ message: t.SUCCESS,
+ description: t.RESEND_SUCCESS,
+ });
+ } else {
+ notificationApi.error({
+ message: t.ERROR,
+ description: resp.errMsg || t.RESEND_FAILED,
+ });
+ }
+ } catch (error) {
+ notificationApi.error({
+ message: t.ERROR,
+ description: t.RESEND_FAILED,
+ });
+ console.error("重发失败:", error);
+ } finally {
+ setLoading(false);
+ // Optionally, you might want to refresh the message detail after resend
+ // or close the modal if resend was successful and you don't need to see details immediately.
+ // For now, we'll keep the modal open and let the user close it.
+ }
+ };
+
+ const topicColumns = [
+ {
+ title: 'Message ID', dataIndex: 'msgId', key: 'msgId', align: 'center',
+ render: (text) => {text}
+ },
+ {title: 'Tag', dataIndex: ['properties', 'TAGS'], key: 'tags', align: 'center'},
+ {title: 'Key', dataIndex: ['properties', 'KEYS'], key: 'keys', align: 'center'},
+ {
+ title: 'StoreTime',
+ dataIndex: 'storeTimestamp',
+ key: 'storeTimestamp',
+ align: 'center',
+ render: (text) => moment(text).format("YYYY-MM-DD HH:mm:ss"),
+ },
+ {
+ title: 'Operation',
+ key: 'operation',
+ align: 'center',
+ render: (_, record) => (
+ showMessageDetail(record.msgId, record.topic)}>
+ {t.MESSAGE_DETAIL}
+
+ ),
+ },
+ ];
+
+ const keyColumns = [
+ {
+ title: 'Message ID', dataIndex: 'msgId', key: 'msgId', align: 'center',
+ render: (text) => {text}
+ },
+ {title: 'Tag', dataIndex: ['properties', 'TAGS'], key: 'tags', align: 'center'},
+ {title: 'Key', dataIndex: ['properties', 'KEYS'], key: 'keys', align: 'center'},
+ {
+ title: 'StoreTime',
+ dataIndex: 'storeTimestamp',
+ key: 'storeTimestamp',
+ align: 'center',
+ render: (text) => moment(text).format("YYYY-MM-DD HH:mm:ss"),
+ },
+ {
+ title: 'Operation',
+ key: 'operation',
+ align: 'center',
+ render: (_, record) => (
+ showMessageDetail(record.msgId, record.topic)}>
+ {t.MESSAGE_DETAIL}
+
+ ),
+ },
+ ];
+
+ return (
+ <>
+ {notificationContextHolder}
+
+
+
+
+ {t.TOTAL_MESSAGES}
+
+
+ {
+ setSelectedTopic(value);
+ onChangeQueryCondition();
+ }}
+ filterOption={(input, option) =>
+ option.children.toLowerCase().indexOf(input.toLowerCase()) >= 0
+ }
+ >
+ {allTopicList.map(topic => (
+ {topic}
+ ))}
+
+
+
+ {
+ setTimepickerBegin(date);
+ onChangeQueryCondition();
+ }}
+ />
+
+
+ {
+ setTimepickerEnd(date);
+ onChangeQueryCondition();
+ }}
+ />
+
+
+ }
+ onClick={() => queryMessagePageByTopic()}>
+ {t.SEARCH}
+
+
+
+
queryMessagePageByTopic(page, pageSize),
+ }}
+ locale={{emptyText: t.NO_MATCH_RESULT}}
+ />
+
+
+
+ {t.ONLY_RETURN_64_MESSAGES}
+
+
+
+ option.children.toLowerCase().indexOf(input.toLowerCase()) >= 0
+ }
+ >
+ {allTopicList.map(topic => (
+ {topic}
+ ))}
+
+
+
+ setKey(e.target.value)}
+ />
+
+
+ }
+ onClick={queryMessageByTopicAndKey}>
+ {t.SEARCH}
+
+
+
+
+
+
+
+
+ {t.MESSAGE_ID_TOPIC_HINT}
+
+
+
+
+ option.children.toLowerCase().indexOf(input.toLowerCase()) >= 0
+ }
+ >
+ {allTopicList.map(topic => (
+ {topic}
+ ))}
+
+
+
+ setMessageId(e.target.value)}
+ />
+
+
+ }
+ onClick={() => showMessageDetail(messageId, selectedTopic)}>
+ {t.SEARCH}
+
+
+
+ {/* Message ID 查询结果通常直接弹窗显示,这里不需要表格 */}
+
+
+
+
+
+ {/* Message Detail Dialog Component */}
+
+
+ >
+
+ );
+};
+
+export default MessageQueryPage;
diff --git a/frontend-new/src/pages/MessageTrace/messagetrace.jsx b/frontend-new/src/pages/MessageTrace/messagetrace.jsx
new file mode 100644
index 0000000..2562143
--- /dev/null
+++ b/frontend-new/src/pages/MessageTrace/messagetrace.jsx
@@ -0,0 +1,429 @@
+/*
+ * 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, notification, Select, Spin, Table, Tabs, Typography} from 'antd';
+import moment from 'moment';
+import {SearchOutlined} from '@ant-design/icons';
+import {useLanguage} from '../../i18n/LanguageContext';
+import MessageTraceDetailViewDialog from "../../components/MessageTraceDetailViewDialog";
+import {remoteApi} from '../../api/remoteApi/remoteApi'; // Import the remoteApi
+
+const {TabPane} = Tabs;
+const {Option} = Select;
+const {Text, Paragraph} = Typography;
+
+const MessageTraceQueryPage = () => {
+ const {t} = useLanguage();
+ const [activeTab, setActiveTab] = useState('messageKey');
+ const [form] = Form.useForm();
+ const [loading, setLoading] = useState(false);
+
+ // 轨迹主题选择
+ const [allTraceTopicList, setAllTraceTopicList] = useState([]);
+ const [selectedTraceTopic, setSelectedTraceTopic] = useState(null); // Initialize as null or a default trace topic if applicable
+
+ // Topic 查询状态
+ const [allTopicList, setAllTopicList] = useState([]);
+ const [selectedTopic, setSelectedTopic] = useState(null);
+ const [key, setKey] = useState('');
+ const [queryMessageByTopicAndKeyResult, setQueryMessageByTopicAndKeyResult] = useState([]);
+
+ // Message ID 查询状态
+ const [messageId, setMessageId] = useState('');
+ const [queryMessageByMessageIdResult, setQueryMessageByMessageIdResult] = useState([]);
+
+ // State for MessageTraceDetailViewDialog
+ const [isTraceDetailViewOpen, setIsTraceDetailViewOpen] = useState(false);
+ const [traceDetailData, setTraceDetailData] = useState(null);
+ const [notificationApi, notificationContextHolder] = notification.useNotification();
+
+ useEffect(() => {
+ const fetchTopics = async () => {
+ setLoading(true);
+ try {
+ const resp = await remoteApi.queryTopic(true);
+
+ if (resp.status === 0) {
+ const topics = resp.data.topicList.sort();
+ setAllTopicList(topics);
+
+ const traceTopics = topics.filter(topic =>
+ !topic.startsWith('%RETRY%') && !topic.startsWith('%DLQ%')
+ );
+ setAllTraceTopicList(traceTopics);
+ // Optionally set a default trace topic if available, e.g., 'RMQ_SYS_TRACE_TOPIC'
+ if (traceTopics.includes('RMQ_SYS_TRACE_TOPIC')) {
+ setSelectedTraceTopic('RMQ_SYS_TRACE_TOPIC');
+ } else if (traceTopics.length > 0) {
+ setSelectedTraceTopic(traceTopics[0]); // Select the first one if no default
+ }
+ } else {
+ notificationApi.error({
+ message: t.ERROR,
+ description: resp.errMsg || t.QUERY_FAILED,
+ });
+ }
+ } catch (error) {
+ notificationApi.error({
+ message: t.ERROR,
+ description: error.message || t.QUERY_FAILED,
+ });
+ } finally {
+ setLoading(false);
+ }
+ };
+
+ fetchTopics();
+ }, [t]);
+
+ const queryMessageByTopicAndKey = async () => {
+ if (!selectedTopic || !key) {
+ notificationApi.warning({
+ message: t.WARNING,
+ description: t.TOPIC_AND_KEY_REQUIRED,
+ });
+ return;
+ }
+ setLoading(true);
+
+ try {
+ const data = await remoteApi.queryMessageByTopicAndKey(selectedTopic, key);
+ if (data.status === 0) {
+ setQueryMessageByTopicAndKeyResult(data.data);
+ if (data.data.length === 0) {
+ notificationApi.info({
+ message: t.NO_RESULT,
+ description: t.NO_MATCH_RESULT,
+ });
+ }
+ } else {
+ notificationApi.error({
+ message: t.ERROR,
+ description: data.errMsg || t.QUERY_FAILED,
+ });
+ setQueryMessageByTopicAndKeyResult([]); // Clear previous results on error
+ }
+ } catch (error) {
+ notificationApi.error({
+ message: t.ERROR,
+ description: error.message || t.QUERY_FAILED,
+ });
+ setQueryMessageByTopicAndKeyResult([]); // Clear previous results on error
+ } finally {
+ setLoading(false);
+ }
+ };
+
+ const queryMessageByMessageId = async (msgIdToQuery, topicToQuery) => {
+ if (!msgIdToQuery) {
+ notificationApi.warning({
+ message: t.WARNING,
+ description: t.MESSAGE_ID_REQUIRED,
+ });
+ return;
+ }
+ setLoading(true);
+
+ try {
+ const res = await remoteApi.queryMessageByMessageId(msgIdToQuery, topicToQuery);
+ if (res.status === 0) {
+ // 确保 data.data.messageView 存在,并将其包装成数组
+ setQueryMessageByMessageIdResult(res.data && res.data.messageView ? [res.data.messageView] : []);
+ } else {
+ notificationApi.error({
+ message: t.ERROR,
+ description: res.errMsg || t.QUERY_FAILED,
+ });
+ setQueryMessageByMessageIdResult([]); // 清除错误时的旧数据
+ }
+ } catch (error) {
+ notificationApi.error({
+ message: t.ERROR,
+ description: error.message || t.QUERY_FAILED,
+ });
+ setQueryMessageByMessageIdResult([]); // 清除错误时的旧数据
+ } finally {
+ setLoading(false);
+ }
+ };
+
+ const queryMessageTraceByMessageId = async (msgId, traceTopic) => {
+ if (!msgId) {
+ notificationApi.warning({
+ message: t.WARNING,
+ description: t.MESSAGE_ID_REQUIRED,
+ });
+ return;
+ }
+ setLoading(true);
+
+ try {
+ const data = await remoteApi.queryMessageTraceByMessageId(msgId, traceTopic || 'RMQ_SYS_TRACE_TOPIC');
+ if (data.status === 0) {
+ setTraceDetailData(data.data);
+ setIsTraceDetailViewOpen(true);
+ } else {
+ notificationApi.error({
+ message: t.ERROR,
+ description: data.errMsg || t.QUERY_FAILED,
+ });
+ setTraceDetailData(null); // Clear previous trace data on error
+ setIsTraceDetailViewOpen(false); // Do not open dialog if data is not available
+ }
+ } catch (error) {
+ notificationApi.error({
+ message: t.ERROR,
+ description: error.message || t.QUERY_FAILED,
+ });
+ setTraceDetailData(null); // Clear previous trace data on error
+ setIsTraceDetailViewOpen(false); // Do not open dialog if data is not available
+ } finally {
+ setLoading(false);
+ }
+ };
+
+ const handleCloseTraceDetailView = () => {
+ setIsTraceDetailViewOpen(false);
+ setTraceDetailData(null);
+ };
+
+ const keyColumns = [
+ {title: 'Message ID', dataIndex: 'msgId', key: 'msgId', align: 'center'},
+ {title: 'Tag', dataIndex: ['properties', 'TAGS'], key: 'tags', align: 'center'},
+ {title: 'Message Key', dataIndex: ['properties', 'KEYS'], key: 'keys', align: 'center'},
+ {
+ title: 'StoreTime',
+ dataIndex: 'storeTimestamp',
+ key: 'storeTimestamp',
+ align: 'center',
+ render: (text) => moment(text).format('YYYY-MM-DD HH:mm:ss'),
+ },
+ {
+ title: 'Operation',
+ key: 'operation',
+ align: 'center',
+ render: (_, record) => (
+ queryMessageTraceByMessageId(record.msgId, selectedTraceTopic)}>
+ {t.MESSAGE_TRACE_DETAIL}
+
+ ),
+ },
+ ];
+
+ const messageIdColumns = [
+ {title: 'Message ID', dataIndex: 'msgId', key: 'msgId', align: 'center'},
+ // 注意:这里的 dataIndex 直接指向了 messageView 内部的属性
+ {title: 'Tag', dataIndex: ['properties', 'TAGS'], key: 'tags', align: 'center'},
+ {title: 'Message Key', dataIndex: ['properties', 'KEYS'], key: 'keys', align: 'center'},
+ {
+ title: 'StoreTime',
+ dataIndex: 'storeTimestamp',
+ key: 'storeTimestamp',
+ align: 'center',
+ render: (text) => moment(text).format('YYYY-MM-DD HH:mm:ss'),
+ },
+ {
+ title: 'Operation',
+ key: 'operation',
+ align: 'center',
+ render: (_, record) => (
+ queryMessageTraceByMessageId(record.msgId, selectedTraceTopic)}>
+ {t.MESSAGE_TRACE_DETAIL}
+
+ ),
+ },
+ ];
+
+ return (
+ <>
+ {notificationContextHolder}
+
+
+
+
{t.TRACE_TOPIC}:}>
+
+ option.children && option.children.toLowerCase().indexOf(input.toLowerCase()) >= 0
+ }
+ >
+ {allTraceTopicList.map(topic => (
+ {topic}
+ ))}
+
+
+ ({t.TRACE_TOPIC_HINT})
+
+
+
+
+
+ {t.ONLY_RETURN_64_MESSAGES}
+
+
+
+ option.children && option.children.toLowerCase().indexOf(input.toLowerCase()) >= 0
+ }
+ >
+ {t.SELECT_TOPIC_PLACEHOLDER}
+ {allTopicList.map(topic => (
+ {topic}
+ ))}
+
+
+
+ setKey(e.target.value)}
+ required
+ />
+
+
+ }
+ onClick={queryMessageByTopicAndKey}>
+ {t.SEARCH}
+
+
+
+
+
+
+
+ {t.MESSAGE_ID_TOPIC_HINT}
+
+
+ {
+ if (option.children && typeof option.children === 'string') {
+ return option.children.toLowerCase().includes(input.toLowerCase());
+ }
+ return false;
+ }}
+ >
+ {t.SELECT_TOPIC_PLACEHOLDER}
+ {allTopicList.map(topic => (
+ {topic}
+ ))}
+
+
+
+ setMessageId(e.target.value)}
+ required
+ />
+
+
+ }
+ onClick={() => queryMessageByMessageId(messageId, selectedTopic)}>
+ {t.SEARCH}
+
+
+
+
+
+
+
+
+
+ {/* MessageTraceDetailViewDialog as a child component */}
+ {isTraceDetailViewOpen && traceDetailData && (
+
+
+ {t.MESSAGE_TRACE_DETAIL}
+
+ {t.CLOSE}
+
+
+
+
+ )}
+
+ >
+
+ );
+};
+
+export default MessageTraceQueryPage;
diff --git a/frontend-new/src/pages/Ops/ops.jsx b/frontend-new/src/pages/Ops/ops.jsx
new file mode 100644
index 0000000..246c590
--- /dev/null
+++ b/frontend-new/src/pages/Ops/ops.jsx
@@ -0,0 +1,183 @@
+/*
+ * 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 { Select, Button, Switch, Input, Typography, Space, message } from 'antd';
+import {remoteApi} from '../../api/remoteApi/remoteApi';
+
+const { Title } = Typography;
+const { Option } = Select;
+
+const Ops = () => {
+ const [namesrvAddrList, setNamesrvAddrList] = useState([]);
+ const [selectedNamesrv, setSelectedNamesrv] = useState('');
+ const [newNamesrvAddr, setNewNamesrvAddr] = useState('');
+ const [useVIPChannel, setUseVIPChannel] = useState(false);
+ const [useTLS, setUseTLS] = useState(false);
+ const [writeOperationEnabled, setWriteOperationEnabled] = useState(true); // Default to true
+ const [messageApi, msgContextHolder] = message.useMessage();
+ useEffect(() => {
+ const fetchOpsData = async () => {
+ const userRole = sessionStorage.getItem("userrole");
+ setWriteOperationEnabled(userRole === null || userRole === "1"); // Assuming "1" means write access
+
+ const resp = await remoteApi.queryOpsHomePage();
+ if (resp.status === 0) {
+ setNamesrvAddrList(resp.data.namesvrAddrList);
+ setUseVIPChannel(resp.data.useVIPChannel);
+ setUseTLS(resp.data.useTLS);
+ setSelectedNamesrv(resp.data.currentNamesrv);
+ } else {
+ messageApi.error(resp.errMsg);
+ }
+ };
+ fetchOpsData();
+ }, []);
+
+ const handleUpdateNameSvrAddr = async () => {
+ if (!selectedNamesrv) {
+ messageApi.warning('请选择一个 NameServer 地址');
+ return;
+ }
+ const resp = await remoteApi.updateNameSvrAddr(selectedNamesrv);
+ if (resp.status === 0) {
+ messageApi.info('UPDATE SUCCESS');
+ } else {
+ messageApi.error(resp.errMsg);
+ }
+ };
+
+ const handleAddNameSvrAddr = async () => {
+ if (!newNamesrvAddr.trim()) {
+ messageApi.warning('请输入新的 NameServer 地址');
+ return;
+ }
+ const resp = await remoteApi.addNameSvrAddr(newNamesrvAddr.trim());
+ if (resp.status === 0) {
+ if (!namesrvAddrList.includes(newNamesrvAddr.trim())) {
+ setNamesrvAddrList([...namesrvAddrList, newNamesrvAddr.trim()]);
+ }
+ setNewNamesrvAddr('');
+ messageApi.info('ADD SUCCESS');
+ } else {
+ messageApi.error(resp.errMsg);
+ }
+ };
+
+ const handleUpdateIsVIPChannel = async (checked) => {
+ setUseVIPChannel(checked); // Optimistic update
+ const resp = await remoteApi.updateIsVIPChannel(checked);
+ if (resp.status === 0) {
+ messageApi.info('UPDATE SUCCESS');
+ } else {
+ messageApi.error(resp.errMsg);
+ setUseVIPChannel(!checked); // Revert on error
+ }
+ };
+
+ const handleUpdateUseTLS = async (checked) => {
+ setUseTLS(checked); // Optimistic update
+ const resp = await remoteApi.updateUseTLS(checked);
+ if (resp.status === 0) {
+ messageApi.info('UPDATE SUCCESS');
+ } else {
+ messageApi.error(resp.errMsg);
+ setUseTLS(!checked); // Revert on error
+ }
+ };
+
+ return (
+ <>
+ {msgContextHolder}
+
+
+
NameServerAddressList
+
+
+ {namesrvAddrList.map((addr) => (
+
+ {addr}
+
+ ))}
+
+
+ {writeOperationEnabled && (
+
+ UPDATE
+
+ )}
+
+ {writeOperationEnabled && (
+
+ setNewNamesrvAddr(e.target.value)}
+ />
+
+ ADD
+
+
+ )}
+
+
+
+
+
IsUseVIPChannel
+
+
+ {writeOperationEnabled && (
+ handleUpdateIsVIPChannel(useVIPChannel)}>
+ UPDATE
+
+ )}
+
+
+
+
+
useTLS
+
+
+ {writeOperationEnabled && (
+ handleUpdateUseTLS(useTLS)}>
+ UPDATE
+
+ )}
+
+
+
+ >
+
+ );
+};
+
+export default Ops;