mirror of
https://github.com/apache/rocketmq-dashboard.git
synced 2025-09-10 03:29:59 +08:00
[GSOC][RIP-78][ISSUES#308] Add part of refactored front-end files (#312)
This commit is contained in:
676
frontend-new/src/pages/Acl/acl.jsx
Normal file
676
frontend-new/src/pages/Acl/acl.jsx
Normal 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;
|
303
frontend-new/src/pages/Cluster/cluster.jsx
Normal file
303
frontend-new/src/pages/Cluster/cluster.jsx
Normal 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;
|
480
frontend-new/src/pages/Consumer/consumer.jsx
Normal file
480
frontend-new/src/pages/Consumer/consumer.jsx
Normal 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;
|
455
frontend-new/src/pages/Dashboard/DashboardPage.jsx
Normal file
455
frontend-new/src/pages/Dashboard/DashboardPage.jsx
Normal 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;
|
703
frontend-new/src/pages/DlqMessage/dlqmessage.jsx
Normal file
703
frontend-new/src/pages/DlqMessage/dlqmessage.jsx
Normal 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;
|
90
frontend-new/src/pages/Login/login.jsx
Normal file
90
frontend-new/src/pages/Login/login.jsx
Normal 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;
|
478
frontend-new/src/pages/Message/message.jsx
Normal file
478
frontend-new/src/pages/Message/message.jsx
Normal 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;
|
429
frontend-new/src/pages/MessageTrace/messagetrace.jsx
Normal file
429
frontend-new/src/pages/MessageTrace/messagetrace.jsx
Normal 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;
|
183
frontend-new/src/pages/Ops/ops.jsx
Normal file
183
frontend-new/src/pages/Ops/ops.jsx
Normal 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;
|
Reference in New Issue
Block a user