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 : '********'} + + + ), + }, + { + 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) => ( + + + handleDeleteUser(record.username)} + okText={t.YES} + cancelText={t.NO} + > + + + + ), + }, + ]; + +// --- 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) => ( + + + handleDeleteAcl(record.subject, record.resource)} + okText={t.YES} + cancelText={t.NO} + > + + + + ), + }, + ]; + +return ( + <> + {msgContextHolder} +
+

{t.ACL_MANAGEMENT}

+ + + + + +
+ + +
+ + {activeTab === 'users' && ( + + )} + + {activeTab === 'acls' && ( +
+ )} + + {/* User Management Modal */} + setIsUserModalVisible(false)} + confirmLoading={loading} + footer={[ + , + , + ]} + > +
+ + + + + (visible ? : )} + /> + + + + + + + + +
+ + {/* ACL Permission Management Modal */} + setIsAclModalVisible(false)} + confirmLoading={loading} + > +
+ + + + + + + + + + {isUpdate ? ( + + ) : ( + + )} + + + + + + + + + + + + +
+ + +);} + +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) => ( + <> + + {/* 传入 record.address */} + + + ), + }, + ]; + + return ( + <> + {contextHolder} + +
+
+ + +
+ +
`${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 ( + <> + + + + + {!sysFlag && writeOperationEnabled && ( + + )} + + ); + }, + }, + ]; + + 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} +
+ +
+
+
+ + 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 && ( + + )} + + {/* 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} +
+ + +
+ +
+ + + + + + + + + + + + +
+ + +
+ +
+ + + + + +
+ +
+ + +
+ +
+ +
+
+ + + + +
+ + + ); +}; + +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) => ( + <> + + + + + ), + }, + ]; + + 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) => ( + <> + + + + + ), + }, + ]; + + return ( + <> + {notificationContextHolder} +
+ + + +
{t.TOTAL_MESSAGES}
+
+
+ + + + + { + setTimepickerBegin(date); + onChangeQueryCondition(); + }} + /> + + + { + setTimepickerEnd(date); + onChangeQueryCondition(); + }} + /> + + + + + + + + + + + +
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} +
+
+
+ + + + + setMessageId(e.target.value)} + /> + + + + + +
+ + + {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) => ( + + ), + }, + ]; + + 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) => ( + + ), + }, + ]; + + return ( + <> + {notificationContextHolder} +
+ + + +
{t.TOTAL_MESSAGES}
+
+
+ + + + + { + setTimepickerBegin(date); + onChangeQueryCondition(); + }} + /> + + + { + setTimepickerEnd(date); + onChangeQueryCondition(); + }} + /> + + + + + +
queryMessagePageByTopic(page, pageSize), + }} + locale={{emptyText: t.NO_MATCH_RESULT}} + /> + + + +
{t.ONLY_RETURN_64_MESSAGES}
+
+
+ + + + + setKey(e.target.value)} + /> + + + + + +
+ + + +
+ {t.MESSAGE_ID_TOPIC_HINT} +
+
+
+ + + + + setMessageId(e.target.value)} + /> + + + + + + {/* 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) => ( + + ), + }, + ]; + + 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) => ( + + ), + }, + ]; + + return ( + <> + {notificationContextHolder} +
+ +
+
+ {t.TRACE_TOPIC}:}> + + + ({t.TRACE_TOPIC_HINT}) + +
+ + + +
{t.ONLY_RETURN_64_MESSAGES}
+
+
+ + + + + setKey(e.target.value)} + required + /> + + + + + +
+ + + +
{t.MESSAGE_ID_TOPIC_HINT}
+
+
+ + + + + setMessageId(e.target.value)} + required + /> + + + + + +
+ + + + + + {/* MessageTraceDetailViewDialog as a child component */} + {isTraceDetailViewOpen && traceDetailData && ( +
+
+ {t.MESSAGE_TRACE_DETAIL} + + +
+
+ )} + + + + ); +}; + +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 + + + + {writeOperationEnabled && ( + + )} + + {writeOperationEnabled && ( + + setNewNamesrvAddr(e.target.value)} + /> + + + )} + +
+ +
+ IsUseVIPChannel + + + {writeOperationEnabled && ( + + )} + +
+ +
+ useTLS + + + {writeOperationEnabled && ( + + )} + +
+
+ + + ); +}; + +export default Ops;