[GSOC][RIP-78][ISSUES#308] Add part of refactored front-end files (#312)

This commit is contained in:
Crazylychee
2025-06-16 13:51:18 +08:00
committed by GitHub
parent bd94e8c4f5
commit 3cbff604e6
9 changed files with 3797 additions and 0 deletions

View File

@@ -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) => (
<span>
{showPassword ? text : '********'}
<Button
type="link"
icon={showPassword ? <EyeInvisibleOutlined /> : <EyeOutlined />}
onClick={() => setShowPassword(!showPassword)}
style={{ marginLeft: 8 }}
>
{showPassword ? t.HIDE : t.VIEW}
</Button>
</span>
),
},
{
title: t.USER_TYPE,
dataIndex: 'userType',
key: 'userType',
},
{
title: t.USER_STATUS,
dataIndex: 'userStatus',
key: 'userStatus',
render: (status) => (
<Tag color={status=== 'enable' ? 'red' : 'green'}>{status}</Tag>
),
},
{
title: t.OPERATION,
key: 'action',
render: (_, record) => (
<Space size="middle">
<Button icon={<EditOutlined />} onClick={() => handleEditUser(record)}>{t.MODIFY}</Button>
<Popconfirm
title={t.CONFIRM_DELETE_USER}
onConfirm={() => handleDeleteUser(record.username)}
okText={t.YES}
cancelText={t.NO}
>
<Button icon={<DeleteOutlined />} danger>{t.DELETE}</Button>
</Popconfirm>
</Space>
),
},
];
// --- 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) => (
<Tag key={index} color="blue">{action}</Tag>
)) : null,
},
{
title: t.SOURCE_IP,
dataIndex: 'sourceIps',
key: 'sourceIps',
},
{
title: t.DECISION,
dataIndex: 'decision',
key: 'decision',
render: (text) => (
<Tag color={text === 'Allow' ? 'green' : 'red'}>{text}</Tag>
),
},
{
title: t.OPERATION,
key: 'action',
render: (_, record) => (
<Space size="middle">
<Button icon={<EditOutlined />} onClick={() => handleEditAcl(record)}>{t.MODIFY}</Button>
<Popconfirm
title={t.CONFIRM_DELETE_ACL}
onConfirm={() => handleDeleteAcl(record.subject, record.resource)}
okText={t.YES}
cancelText={t.NO}
>
<Button icon={<DeleteOutlined />} danger>{t.DELETE}</Button>
</Popconfirm>
</Space>
),
},
];
return (
<>
{msgContextHolder}
<div style={{ padding: 24 }}>
<h2>{t.ACL_MANAGEMENT}</h2>
<Tabs activeKey={activeTab} onChange={setActiveTab}>
<TabPane tab={t.ACL_USERS} key="users" />
<TabPane tab={t.ACL_PERMISSIONS} key="acls" />
</Tabs>
<div style={{ marginBottom: 16, display: 'flex', justifyContent: 'space-between' }}>
<Button type="primary" onClick={activeTab === 'users' ? handleAddUser : handleAddAcl}>
{activeTab === 'users' ? t.ADD_USER : t.ADD_ACL_PERMISSION}
</Button>
<Search
placeholder={t.SEARCH_PLACEHOLDER}
allowClear
onSearch={handleSearch}
style={{ width: 300 }}
/>
</div>
{activeTab === 'users' && (
<Table
columns={userColumns}
dataSource={userListData}
loading={loading}
pagination={{ pageSize: 10 }}
rowKey="username"
/>
)}
{activeTab === 'acls' && (
<Table
columns={aclColumns}
dataSource={aclListData}
loading={loading}
pagination={{ pageSize: 10 }}
rowKey="key"
/>
)}
{/* User Management Modal */}
<Modal
title={currentUser ? t.EDIT_USER : t.ADD_USER}
visible={isUserModalVisible}
onOk={handleUserModalOk}
onCancel={() => setIsUserModalVisible(false)}
confirmLoading={loading}
footer={[
<Button key="cancel" onClick={() => setIsUserModalVisible(false)}>
{t.CANCEL}
</Button>,
<Button key="submit" type="primary" onClick={handleUserModalOk} loading={loading}>
{t.CONFIRM}
</Button>,
]}
>
<Form
form={userForm}
layout="vertical"
name="user_form"
initialValues={{ userStatus: 'enable' }}
>
<Form.Item
name="username"
label={t.USERNAME}
rules={[{ required: true, message: t.PLEASE_ENTER_USERNAME }]}
>
<Input disabled={!!currentUser} />
</Form.Item>
<Form.Item
name="password"
label={t.PASSWORD}
rules={[{ required: !currentUser, message: t.PLEASE_ENTER_PASSWORD }]}
>
<Input.Password
placeholder={t.PASSWORD}
iconRender={visible => (visible ? <EyeOutlined /> : <EyeInvisibleOutlined />)}
/>
</Form.Item>
<Form.Item
name="userType"
label={t.USER_TYPE}
rules={[{ required: true, message: t.PLEASE_SELECT_USER_TYPE }]}
>
<Select mode="single" placeholder="Super, Normal" style={{ width: '100%' }}>
<Select.Option value="Super">Super</Select.Option>
<Select.Option value="Normal">Normal</Select.Option>
</Select>
</Form.Item>
<Form.Item
name="userStatus"
label={t.USER_STATUS}
rules={[{ required: true, message: t.PLEASE_SELECT_USER_STATUS }]}
>
<Select mode="single" placeholder="enable, disable" style={{ width: '100%' }}>
<Select.Option value="enable">enable</Select.Option>
<Select.Option value="disable">disable</Select.Option>
</Select>
</Form.Item>
</Form>
</Modal>
{/* ACL Permission Management Modal */}
<Modal
title={currentAcl ? t.EDIT_ACL_PERMISSION : t.ADD_ACL_PERMISSION}
visible={isAclModalVisible}
onOk={handleAclModalOk}
onCancel={() => setIsAclModalVisible(false)}
confirmLoading={loading}
>
<Form
form={aclForm}
layout="vertical"
name="acl_form"
>
<Form.Item
name="subject"
label={t.SUBJECT_LABEL}
rules={[{ required: true, message: t.PLEASE_ENTER_SUBJECT }]}
>
<SubjectInput disabled={!!currentAcl} />
</Form.Item>
<Form.Item
name="policyType"
label={t.POLICY_TYPE}
rules={[{ required: true, message: t.PLEASE_ENTER_POLICY_TYPE }]}
>
<Select mode="single" disabled={isUpdate} placeholder="policyType" style={{ width: '100%' }}>
<Select.Option value="Custom">Custom</Select.Option>
<Select.Option value="Default">Default</Select.Option>
</Select>
</Form.Item>
<Form.Item
name="resource"
label={t.RESOURCE}
rules={[{ required: true, message: t.PLEASE_ADD_RESOURCE }]}
>
{isUpdate ? (
<Input disabled={isUpdate} />
) : (
<ResourceInput />
)}
</Form.Item>
<Form.Item
name="actions"
label={t.OPERATION_TYPE}
>
<Select mode="multiple" placeholder="action" style={{ width: '100%' }}>
<Select.Option value="All">All</Select.Option>
<Select.Option value="Pub">Pub</Select.Option>
<Select.Option value="Sub">Sub</Select.Option>
<Select.Option value="Create">Create</Select.Option>
<Select.Option value="Update">Update</Select.Option>
<Select.Option value="Delete">Delete</Select.Option>
<Select.Option value="Get">Get</Select.Option>
<Select.Option value="List">List</Select.Option>
</Select>
</Form.Item>
<Form.Item
name="sourceIps"
label={t.SOURCE_IP}
rules={[
{
validator: validateIp,
},
]}
>
<Select
mode="tags"
style={{ width: '100%' }}
placeholder={t.ENTER_IP_HINT}
onChange={handleIpChange}
onDeselect={handleIpDeselect}
value={ips}
tokenSeparators={[',', ' ']}
>
<Select.Option value="192.168.1.1">192.168.1.1</Select.Option>
<Select.Option value="0.0.0.0">0.0.0.0</Select.Option>
<Select.Option value="127.0.0.1">127.0.0.1</Select.Option>
</Select>
</Form.Item>
<Form.Item
name="decision"
label={t.DECISION}
rules={[{ required: true, message: t.PLEASE_ENTER_DECISION }]}
>
<Select mode="single" placeholder="Allow, Deny" style={{ width: '100%' }}>
<Select.Option value="Allow">Allow</Select.Option>
<Select.Option value="Deny">Deny</Select.Option>
</Select>
</Form.Item>
</Form>
</Modal>
</div>
</>
);}
export default Acl;

View File

@@ -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) => (
<>
<Button size="small" type="primary"
onClick={() => showDetail(record.brokerName, record.brokerId, record)}
style={{marginRight: 8}}>
{t.STATUS}
</Button>
{/* 传入 record.address */}
<Button size="small" type="primary"
onClick={() => showConfig(record.address, record.brokerName, record.brokerId)}>
{t.CONFIG}
</Button>
</>
),
},
];
return (
<>
{contextHolder}
<Spin spinning={loading} tip={t.LOADING}>
<div style={{padding: 24}}>
<div style={{marginBottom: 16, display: 'flex', alignItems: 'center'}}>
<label style={{marginRight: 8}}>{t.CLUSTER}:</label>
<Select
style={{width: 300}}
placeholder={t.SELECT_CLUSTER || "Please select a cluster"}
value={selectedCluster}
onChange={handleChangeCluster}
allowClear
>
{clusterNames.map((name) => (
<Option key={name} value={name}>
{name}
</Option>
))}
</Select>
</div>
<Table
dataSource={instances}
columns={columns}
rowKey={(record) => `${record.brokerName}-${record.brokerId}`}
pagination={false}
bordered
size="middle"
/>
<Modal
title={`${t.BROKER} [${currentBrokerName}][${currentIndex}]`}
open={detailModalVisible}
footer={null}
onCancel={() => setDetailModalVisible(false)}
width={800}
bodyStyle={{maxHeight: '60vh', overflowY: 'auto'}}
>
<Table
dataSource={Object.entries(currentDetail).map(([key, value]) => ({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"
/>
</Modal>
<Modal
title={`${t.BROKER} [${currentBrokerName}][${currentIndex}]`}
open={configModalVisible}
footer={null}
onCancel={() => setConfigModalVisible(false)}
width={800}
bodyStyle={{maxHeight: '60vh', overflowY: 'auto'}}
>
<Table
dataSource={Object.entries(currentConfig).map(([key, value]) => ({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"
/>
</Modal>
</div>
</Spin>
</>
);
};
export default Cluster;

View File

@@ -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: <a onClick={() => handleSort('group')}>{t.SUBSCRIPTION_GROUP}</a>,
dataIndex: 'group',
key: 'group',
align: 'center',
render: (text) => {
const sysFlag = text.startsWith('%SYS%');
return (
<span style={{color: sysFlag ? 'red' : ''}}>
{sysFlag ? text.substring(5) : text}
</span>
);
},
},
{
title: <a onClick={() => handleSort('count')}>{t.QUANTITY}</a>,
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: <a onClick={() => handleSort('consumeTps')}>TPS</a>,
dataIndex: 'consumeTps',
key: 'consumeTps',
align: 'center',
},
{
title: <a onClick={() => handleSort('diffTotal')}>{t.DELAY}</a>,
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 (
<>
<Button
type="primary"
size="small"
style={{marginRight: 8, marginBottom: 8}}
onClick={() => handleClient(record.group, record.address)}
>
{t.CLIENT}
</Button>
<Button
type="primary"
size="small"
style={{marginRight: 8, marginBottom: 8}}
onClick={() => handleDetail(record.group, record.address)}
>
{t.CONSUME_DETAIL}
</Button>
<Button
type="primary"
size="small"
style={{marginRight: 8, marginBottom: 8}}
onClick={() => handleUpdateConfigDialog(record.group)}
>
{t.CONFIG}
</Button>
<Button
type="primary"
size="small"
style={{marginRight: 8, marginBottom: 8}}
onClick={() => handleRefreshConsumerGroup(record.group)}
>
{t.REFRESH}
</Button>
{!sysFlag && writeOperationEnabled && (
<Button
type="primary"
danger
size="small"
style={{marginRight: 8, marginBottom: 8}}
onClick={() => handleDelete(record.group)}
>
{t.DELETE}
</Button>
)}
</>
);
},
},
];
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}
<div style={{padding: '20px'}}>
<Spin spinning={loading} tip={t.LOADING}>
<div style={{marginBottom: '20px'}}>
<div style={{display: 'flex', alignItems: 'center', gap: '15px'}}>
<div style={{display: 'flex', alignItems: 'center'}}>
<label style={{marginRight: '8px'}}>{t.SUBSCRIPTION_GROUP}:</label>
<Input
style={{width: '200px'}}
value={filterStr}
onChange={(e) => handleFilterInputChange(e.target.value)}
/>
</div>
<Checkbox checked={filterNormal}
onChange={(e) => handleTypeFilterChange('normal', e.target.checked)}>
{t.NORMAL}
</Checkbox>
{rmqVersion && (
<Checkbox checked={filterFIFO}
onChange={(e) => handleTypeFilterChange('fifo', e.target.checked)}>
{t.FIFO}
</Checkbox>
)}
<Checkbox checked={filterSystem}
onChange={(e) => handleTypeFilterChange('system', e.target.checked)}>
{t.SYSTEM}
</Checkbox>
{writeOperationEnabled && (
<Button type="primary" onClick={handleOpenAddDialog}>
{t.ADD} / {t.UPDATE}
</Button>
)}
<Button type="primary" onClick={handleRefreshConsumerData}>
{t.REFRESH}
</Button>
{/*<Switch*/}
{/* checked={intervalProcessSwitch}*/}
{/* onChange={(checked) => setIntervalProcessSwitch(checked)}*/}
{/* checkedChildren={t.AUTO_REFRESH}*/}
{/* unCheckedChildren={t.AUTO_REFRESH}*/}
{/*/>*/}
</div>
</div>
<Table
dataSource={consumerGroupShowList}
columns={columns}
rowKey="group"
bordered
pagination={paginationConf}
onChange={handleTableChange}
sortDirections={['ascend', 'descend']}
/>
</Spin>
<ClientInfoModal
visible={showClientInfo}
group={selectedGroup}
address={selectedAddress}
onCancel={() => setShowClientInfo(false)}
/>
<ConsumerDetailModal
visible={showConsumeDetail}
group={selectedGroup}
address={selectedAddress}
onCancel={() => setShowConsumeDetail(false)}
/>
<ConsumerConfigModal
visible={showConfig}
isAddConfig={isAddConfig}
group={selectedGroup}
onCancel={closeConfigModal}
setIsAddConfig={setIsAddConfig}
onSuccess={loadConsumerGroups}
/>
<DeleteConsumerModal
visible={showDeleteModal}
group={selectedGroup}
onCancel={() => setShowDeleteModal(false)}
onSuccess={loadConsumerGroups}
/>
</div>
</>
);
};
export default ConsumerGroupList;

View File

@@ -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}
<div style={{padding: '20px'}}>
<Spin spinning={loading} tip={t.LOADING}>
<Row gutter={[16, 16]} style={{marginBottom: '20px'}}>
<Col span={12}>
<Card title={t.BROKER_OVERVIEW} bordered>
<Table
columns={brokerColumns}
dataSource={brokerTableData}
rowKey="key"
pagination={false}
size="small"
bordered
scroll={{y: 240}}
/>
</Card>
</Col>
<Col span={12}>
<Card title={t.DASHBOARD_DATE_SELECTION} bordered>
<DatePicker
format="YYYY-MM-DD"
value={date}
onChange={setDate}
allowClear
style={{width: '100%'}}
/>
</Card>
</Col>
</Row>
<Row gutter={[16, 16]} style={{marginBottom: '20px'}}>
<Col span={12}>
<Card title={`${t.BROKER} TOP 10`} bordered>
<div ref={barChartRef} style={{height: 300}}/>
</Card>
</Col>
<Col span={12}>
<Card title={`${t.BROKER} 5min ${t.TREND}`} bordered>
<div ref={lineChartRef} style={{height: 300}}/>
</Card>
</Col>
</Row>
<Row gutter={[16, 16]}>
<Col span={12}>
<Card title={`${t.TOPIC} TOP 10`} bordered>
<div ref={topicBarChartRef} style={{height: 300}}/>
</Card>
</Col>
<Col span={12}>
<Card title={`${t.TOPIC} 5min ${t.TREND}`} bordered>
<div style={{marginBottom: '10px'}}>
<Select
showSearch
style={{width: '100%'}}
placeholder={t.SELECT_TOPIC_PLACEHOLDER}
value={selectedTopic}
onChange={setSelectedTopic}
filterOption={(input, option) =>
option.children.toLowerCase().indexOf(input.toLowerCase()) >= 0
}
>
{topicNames.map(topic => (
<Option key={topic} value={topic}>{topic}</Option>
))}
</Select>
</div>
<div ref={topicLineChartRef} style={{height: 300}}/>
</Card>
</Col>
</Row>
</Spin>
</div>
</>
);
};
export default DashboardPage;

View File

@@ -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: (
<DlqMessageDetailViewDialog
ngDialogData={{messageView: resp.data}}
/>
),
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: (
<Checkbox
checked={checkedAll}
onChange={handleSelectAll}
disabled={messageShowList.length === 0}
/>
),
dataIndex: 'checked',
key: 'checkbox',
align: 'center',
render: (checked, record) => (
<Checkbox
checked={checked}
onChange={(e) => 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) => (
<>
<Button type="primary" size="small" style={{marginRight: 8, marginBottom: 8}}
onClick={() => queryDlqMessageDetail(record.msgId, selectedConsumerGroup)}>
{t.MESSAGE_DETAIL}
</Button>
<Button type="primary" size="small" style={{marginRight: 8, marginBottom: 8}}
onClick={() => resendDlqMessage(record, selectedConsumerGroup)}>
{t.RESEND_MESSAGE}
</Button>
<Button type="primary" size="small" style={{marginBottom: 8}}
onClick={() => exportDlqMessage(record.msgId, selectedConsumerGroup)}>
{t.EXPORT}
</Button>
</>
),
},
];
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) => (
<>
<Button type="primary" size="small" style={{marginRight: 8, marginBottom: 8}}
onClick={() => queryDlqMessageDetail(record.msgId, selectedConsumerGroup)}>
{t.MESSAGE_DETAIL}
</Button>
<Button type="primary" size="small" style={{marginRight: 8, marginBottom: 8}}
onClick={() => resendDlqMessage(record, selectedConsumerGroup)}>
{t.RESEND_MESSAGE}
</Button>
<Button type="primary" size="small" style={{marginBottom: 8}}
onClick={() => exportDlqMessage(record.msgId, selectedConsumerGroup)}>
{t.EXPORT}
</Button>
</>
),
},
];
return (
<>
{notificationContextHolder}
<div style={{padding: '20px'}}>
<Spin spinning={loading} tip="加载中...">
<Tabs activeKey={activeTab} onChange={setActiveTab} centered>
<TabPane tab={t.CONSUMER} key="consumer">
<h5 style={{margin: '15px 0'}}>{t.TOTAL_MESSAGES}</h5>
<div style={{padding: '20px', minHeight: '600px'}}>
<Form layout="inline" form={form} style={{marginBottom: '20px'}}>
<Form.Item label={t.CONSUMER}>
<Select
showSearch
style={{width: 300}}
placeholder={t.SELECT_CONSUMER_GROUP_PLACEHOLDER}
value={selectedConsumerGroup}
onChange={(value) => {
setSelectedConsumerGroup(value);
onChangeQueryCondition();
}}
filterOption={(input, option) =>
option.children.toLowerCase().indexOf(input.toLowerCase()) >= 0
}
>
{allConsumerGroupList.map(group => (
<Option key={group} value={group}>{group}</Option>
))}
</Select>
</Form.Item>
<Form.Item label={t.BEGIN}>
<DatePicker
showTime
format="YYYY-MM-DD HH:mm:ss"
value={timepickerBegin}
onChange={(date) => {
setTimepickerBegin(date);
onChangeQueryCondition();
}}
/>
</Form.Item>
<Form.Item label={t.END}>
<DatePicker
showTime
format="YYYY-MM-DD HH:mm:ss"
value={timepickerEnd}
onChange={(date) => {
setTimepickerEnd(date);
onChangeQueryCondition();
}}
/>
</Form.Item>
<Form.Item>
<Button type="primary" icon={<SearchOutlined/>}
onClick={() => queryDlqMessageByConsumerGroup()}>
{t.SEARCH}
</Button>
</Form.Item>
<Form.Item>
<Button
id="batchResendBtn"
type="primary"
icon={<SendOutlined/>}
onClick={batchResendDlqMessage}
disabled={selectedMessageIds.size === 0}
>
{t.BATCH_RESEND}
</Button>
</Form.Item>
<Form.Item>
<Button
id="batchExportBtn"
type="primary"
icon={<ExportOutlined/>}
onClick={batchExportDlqMessage}
disabled={selectedMessageIds.size === 0}
>
{t.BATCH_EXPORT}
</Button>
</Form.Item>
</Form>
<Table
columns={consumerColumns}
dataSource={messageShowList}
rowKey="msgId"
bordered
pagination={{
current: paginationConf.current,
pageSize: paginationConf.pageSize,
total: paginationConf.total,
onChange: (page, pageSize) => queryDlqMessageByConsumerGroup(page, pageSize),
showSizeChanger: true, // Allow changing page size
pageSizeOptions: ['10', '20', '50', '100'], // Customizable page size options
}}
locale={{emptyText: t.NO_MATCH_RESULT}}
/>
</div>
</TabPane>
<TabPane tab="Message ID" key="messageId">
<h5 style={{margin: '15px 0'}}>
{t.MESSAGE_ID_CONSUMER_GROUP_HINT}
</h5>
<div style={{padding: '20px', minHeight: '600px'}}>
<Form layout="inline" style={{marginBottom: '20px'}}>
<Form.Item label={t.CONSUMER}>
<Select
showSearch
style={{width: 300}}
placeholder={t.SELECT_CONSUMER_GROUP_PLACEHOLDER}
value={selectedConsumerGroup}
onChange={setSelectedConsumerGroup}
filterOption={(input, option) =>
option.children.toLowerCase().indexOf(input.toLowerCase()) >= 0
}
>
{allConsumerGroupList.map(group => (
<Option key={group} value={group}>{group}</Option>
))}
</Select>
</Form.Item>
<Form.Item label="MessageId:">
<Input
style={{width: 450}}
value={messageId}
onChange={(e) => setMessageId(e.target.value)}
/>
</Form.Item>
<Form.Item>
<Button type="primary" icon={<SearchOutlined/>}
onClick={queryDlqMessageByMessageId}>
{t.SEARCH}
</Button>
</Form.Item>
</Form>
<Table
columns={messageIdColumns}
dataSource={queryDlqMessageByMessageIdResult}
rowKey="msgId"
bordered
pagination={false}
locale={{emptyText: t.NO_MATCH_RESULT}}
/>
</div>
</TabPane>
{modalContextHolder}
</Tabs>
</Spin>
</div>
</>
);
};
export default DlqMessageQueryPage;

View File

@@ -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}
<div style={{
maxWidth: 400,
margin: '100px auto',
padding: 24,
boxShadow: '0 2px 8px #f0f1f2',
borderRadius: 8
}}>
<Title level={3} style={{textAlign: 'center', marginBottom: 24}}>
WELCOME
</Title>
<Form
form={form}
name="login_form"
layout="vertical"
onFinish={onFinish}
initialValues={{username: '', password: ''}}
>
<Form.Item
label="用户名"
name="username"
rules={[{required: true, message: '请输入用户名'}]}
>
<Input placeholder="请输入用户名"/>
</Form.Item>
<Form.Item
label="密码"
name="password"
rules={[{required: true, message: '请输入密码'}]}
>
<Input.Password placeholder="请输入密码"/>
</Form.Item>
<Form.Item>
<Button type="primary" htmlType="submit" block>
登录
</Button>
</Form.Item>
</Form>
</div>
</>
);
};
export default Login;

View File

@@ -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 copyable>{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) => (
<Button type="primary" size="small" onClick={() => showMessageDetail(record.msgId, record.topic)}>
{t.MESSAGE_DETAIL}
</Button>
),
},
];
const keyColumns = [
{
title: 'Message ID', dataIndex: 'msgId', key: 'msgId', align: 'center',
render: (text) => <Text copyable>{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) => (
<Button type="primary" size="small" onClick={() => showMessageDetail(record.msgId, record.topic)}>
{t.MESSAGE_DETAIL}
</Button>
),
},
];
return (
<>
{notificationContextHolder}
<div style={{padding: '20px'}}>
<Spin spinning={loading} tip={t.LOADING_DATA}>
<Tabs activeKey={activeTab} onChange={setActiveTab} centered>
<TabPane tab="Topic" key="topic">
<h5 style={{margin: '15px 0'}}>{t.TOTAL_MESSAGES}</h5>
<div style={{padding: '20px', minHeight: '600px'}}>
<Form layout="inline" form={form} style={{marginBottom: '20px'}}>
<Form.Item label={t.TOPIC}>
<Select
showSearch
style={{width: 300}}
placeholder={t.SELECT_TOPIC_PLACEHOLDER}
value={selectedTopic}
onChange={(value) => {
setSelectedTopic(value);
onChangeQueryCondition();
}}
filterOption={(input, option) =>
option.children.toLowerCase().indexOf(input.toLowerCase()) >= 0
}
>
{allTopicList.map(topic => (
<Option key={topic} value={topic}>{topic}</Option>
))}
</Select>
</Form.Item>
<Form.Item label={t.BEGIN}>
<DatePicker
showTime
format="YYYY-MM-DD HH:mm:ss"
value={timepickerBegin}
onChange={(date) => {
setTimepickerBegin(date);
onChangeQueryCondition();
}}
/>
</Form.Item>
<Form.Item label={t.END}>
<DatePicker
showTime
format="YYYY-MM-DD HH:mm:ss"
value={timepickerEnd}
onChange={(date) => {
setTimepickerEnd(date);
onChangeQueryCondition();
}}
/>
</Form.Item>
<Form.Item>
<Button type="primary" icon={<SearchOutlined/>}
onClick={() => queryMessagePageByTopic()}>
{t.SEARCH}
</Button>
</Form.Item>
</Form>
<Table
columns={topicColumns}
dataSource={messageShowList}
rowKey="msgId"
bordered
pagination={{
current: paginationConf.current,
pageSize: paginationConf.pageSize,
total: paginationConf.total,
onChange: (page, pageSize) => queryMessagePageByTopic(page, pageSize),
}}
locale={{emptyText: t.NO_MATCH_RESULT}}
/>
</div>
</TabPane>
<TabPane tab="Message Key" key="messageKey">
<h5 style={{margin: '15px 0'}}>{t.ONLY_RETURN_64_MESSAGES}</h5>
<div style={{padding: '20px', minHeight: '600px'}}>
<Form layout="inline" style={{marginBottom: '20px'}}>
<Form.Item label="Topic:">
<Select
showSearch
style={{width: 300}}
placeholder={t.SELECT_TOPIC_PLACEHOLDER}
value={selectedTopic}
onChange={setSelectedTopic}
filterOption={(input, option) =>
option.children.toLowerCase().indexOf(input.toLowerCase()) >= 0
}
>
{allTopicList.map(topic => (
<Option key={topic} value={topic}>{topic}</Option>
))}
</Select>
</Form.Item>
<Form.Item label="Key:">
<Input
style={{width: 450}}
value={key}
onChange={(e) => setKey(e.target.value)}
/>
</Form.Item>
<Form.Item>
<Button type="primary" icon={<SearchOutlined/>}
onClick={queryMessageByTopicAndKey}>
{t.SEARCH}
</Button>
</Form.Item>
</Form>
<Table
columns={keyColumns}
dataSource={queryMessageByTopicAndKeyResult}
rowKey="msgId"
bordered
pagination={false}
locale={{emptyText: t.NO_MATCH_RESULT}}
/>
</div>
</TabPane>
<TabPane tab="Message ID" key="messageId">
<h5 style={{margin: '15px 0'}}>
{t.MESSAGE_ID_TOPIC_HINT}
</h5>
<div style={{padding: '20px', minHeight: '600px'}}>
<Form layout="inline" style={{marginBottom: '20px'}}>
<Form.Item label="Topic:">
<Select
showSearch
style={{width: 300}}
placeholder={t.SELECT_TOPIC_PLACEHOLDER}
value={selectedTopic}
onChange={setSelectedTopic}
filterOption={(input, option) =>
option.children.toLowerCase().indexOf(input.toLowerCase()) >= 0
}
>
{allTopicList.map(topic => (
<Option key={topic} value={topic}>{topic}</Option>
))}
</Select>
</Form.Item>
<Form.Item label="MessageId:">
<Input
style={{width: 450}}
value={messageId}
onChange={(e) => setMessageId(e.target.value)}
/>
</Form.Item>
<Form.Item>
<Button type="primary" icon={<SearchOutlined/>}
onClick={() => showMessageDetail(messageId, selectedTopic)}>
{t.SEARCH}
</Button>
</Form.Item>
</Form>
{/* Message ID 查询结果通常直接弹窗显示,这里不需要表格 */}
</div>
</TabPane>
</Tabs>
</Spin>
{/* Message Detail Dialog Component */}
<MessageDetailViewDialog
visible={isMessageDetailModalVisible}
onCancel={handleCloseMessageDetailModal}
messageId={currentMessageIdForDetail}
topic={currentTopicForDetail}
onResendMessage={handleResendMessage} // Pass the resend function
/>
</div>
</>
);
};
export default MessageQueryPage;

View File

@@ -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) => (
<Button type="primary" size="small"
onClick={() => queryMessageTraceByMessageId(record.msgId, selectedTraceTopic)}>
{t.MESSAGE_TRACE_DETAIL}
</Button>
),
},
];
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) => (
<Button type="primary" size="small"
onClick={() => queryMessageTraceByMessageId(record.msgId, selectedTraceTopic)}>
{t.MESSAGE_TRACE_DETAIL}
</Button>
),
},
];
return (
<>
{notificationContextHolder}
<div style={{padding: '20px'}}>
<Spin spinning={loading} tip="加载中...">
<div style={{marginBottom: '20px', borderBottom: '1px solid #f0f0f0', paddingBottom: '15px'}}>
<Form layout="inline">
<Form.Item label={<Text strong>{t.TRACE_TOPIC}:</Text>}>
<Select
showSearch
style={{minWidth: 300}}
placeholder={t.SELECT_TRACE_TOPIC_PLACEHOLDER}
value={selectedTraceTopic}
onChange={setSelectedTraceTopic}
filterOption={(input, option) =>
option.children && option.children.toLowerCase().indexOf(input.toLowerCase()) >= 0
}
>
{allTraceTopicList.map(topic => (
<Option key={topic} value={topic}>{topic}</Option>
))}
</Select>
</Form.Item>
<Text type="secondary" style={{marginLeft: 10}}>({t.TRACE_TOPIC_HINT})</Text>
</Form>
</div>
<Tabs activeKey={activeTab} onChange={setActiveTab} centered>
<TabPane tab="Message Key" key="messageKey">
<h5 style={{margin: '15px 0'}}>{t.ONLY_RETURN_64_MESSAGES}</h5>
<div style={{padding: '20px', minHeight: '600px'}}>
<Form layout="inline" form={form} style={{marginBottom: '20px'}}>
<Form.Item label="Topic:">
<Select
showSearch
style={{width: 300}}
placeholder={t.SELECT_TOPIC_PLACEHOLDER}
value={selectedTopic}
onChange={setSelectedTopic}
required
filterOption={(input, option) =>
option.children && option.children.toLowerCase().indexOf(input.toLowerCase()) >= 0
}
>
<Option value="">{t.SELECT_TOPIC_PLACEHOLDER}</Option>
{allTopicList.map(topic => (
<Option key={topic} value={topic}>{topic}</Option>
))}
</Select>
</Form.Item>
<Form.Item label="Key:">
<Input
style={{width: 450}}
value={key}
onChange={(e) => setKey(e.target.value)}
required
/>
</Form.Item>
<Form.Item>
<Button type="primary" icon={<SearchOutlined/>}
onClick={queryMessageByTopicAndKey}>
{t.SEARCH}
</Button>
</Form.Item>
</Form>
<Table
columns={keyColumns}
dataSource={queryMessageByTopicAndKeyResult}
rowKey="msgId"
bordered
pagination={false}
locale={{emptyText: t.NO_MATCH_RESULT}}
/>
</div>
</TabPane>
<TabPane tab="Message ID" key="messageId">
<h5 style={{margin: '15px 0'}}>{t.MESSAGE_ID_TOPIC_HINT}</h5>
<div style={{padding: '20px', minHeight: '600px'}}>
<Form layout="inline" style={{marginBottom: '20px'}}>
<Form.Item label="Topic:">
<Select
showSearch
style={{width: 300}}
placeholder={t.SELECT_TOPIC_PLACEHOLDER}
value={selectedTopic}
onChange={setSelectedTopic}
required
filterOption={(input, option) => {
if (option.children && typeof option.children === 'string') {
return option.children.toLowerCase().includes(input.toLowerCase());
}
return false;
}}
>
<Option value="">{t.SELECT_TOPIC_PLACEHOLDER}</Option>
{allTopicList.map(topic => (
<Option key={topic} value={topic}>{topic}</Option>
))}
</Select>
</Form.Item>
<Form.Item label="MessageId:">
<Input
style={{width: 450}}
value={messageId}
onChange={(e) => setMessageId(e.target.value)}
required
/>
</Form.Item>
<Form.Item>
<Button type="primary" icon={<SearchOutlined/>}
onClick={() => queryMessageByMessageId(messageId, selectedTopic)}>
{t.SEARCH}
</Button>
</Form.Item>
</Form>
<Table
columns={messageIdColumns}
dataSource={queryMessageByMessageIdResult}
rowKey="msgId"
bordered
pagination={false}
locale={{emptyText: t.NO_MATCH_RESULT}}
/>
</div>
</TabPane>
</Tabs>
</Spin>
{/* MessageTraceDetailViewDialog as a child component */}
{isTraceDetailViewOpen && traceDetailData && (
<div style={{
position: 'fixed',
top: 0,
left: 0,
right: 0,
bottom: 0,
backgroundColor: 'rgba(0, 0, 0, 0.5)',
display: 'flex',
justifyContent: 'center',
alignItems: 'center',
zIndex: 1000,
}}>
<div style={{
backgroundColor: '#fff',
padding: '20px',
borderRadius: '8px',
width: '80%',
maxHeight: '90%',
overflowY: 'auto',
position: 'relative'
}}>
<Typography.Title level={4}
style={{marginBottom: '20px'}}>{t.MESSAGE_TRACE_DETAIL}</Typography.Title>
<Button
onClick={handleCloseTraceDetailView}
style={{
position: 'absolute',
top: '20px',
right: '20px',
}}
>
{t.CLOSE}
</Button>
<MessageTraceDetailViewDialog
ngDialogData={traceDetailData}
/>
</div>
</div>
)}
</div>
</>
);
};
export default MessageTraceQueryPage;

View File

@@ -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}
<div style={{padding: 24}}>
<div style={{marginBottom: 24}}>
<Title level={4}>NameServerAddressList</Title>
<Space wrap align="start">
<Select
style={{minWidth: 400, maxWidth: 500}}
value={selectedNamesrv}
onChange={setSelectedNamesrv}
disabled={!writeOperationEnabled}
placeholder="请选择 NameServer 地址"
>
{namesrvAddrList.map((addr) => (
<Option key={addr} value={addr}>
{addr}
</Option>
))}
</Select>
{writeOperationEnabled && (
<Button type="primary" onClick={handleUpdateNameSvrAddr}>
UPDATE
</Button>
)}
{writeOperationEnabled && (
<Input.Group compact style={{minWidth: 400}}>
<Input
style={{width: 300}}
placeholder="NamesrvAddr"
value={newNamesrvAddr}
onChange={(e) => setNewNamesrvAddr(e.target.value)}
/>
<Button type="primary" onClick={handleAddNameSvrAddr}>
ADD
</Button>
</Input.Group>
)}
</Space>
</div>
<div style={{marginBottom: 24}}>
<Title level={4}>IsUseVIPChannel</Title>
<Space align="center">
<Switch
checked={useVIPChannel}
onChange={handleUpdateIsVIPChannel}
disabled={!writeOperationEnabled}
/>
{writeOperationEnabled && (
<Button type="primary" onClick={() => handleUpdateIsVIPChannel(useVIPChannel)}>
UPDATE
</Button>
)}
</Space>
</div>
<div style={{marginBottom: 24}}>
<Title level={4}>useTLS</Title>
<Space align="center">
<Switch
checked={useTLS}
onChange={handleUpdateUseTLS}
disabled={!writeOperationEnabled}
/>
{writeOperationEnabled && (
<Button type="primary" onClick={() => handleUpdateUseTLS(useTLS)}>
UPDATE
</Button>
)}
</Space>
</div>
</div>
</>
);
};
export default Ops;