Compare commits

...

4 Commits

Author SHA1 Message Date
Crazylychee
07793d8aae [ISSUE #329] Add Frontend Proxy Component Support (#336) 2025-07-05 20:53:57 +08:00
Crazylychee
4b9ed97f8f [ISSUE #332] Add configuration options for login information, supporting ACL or file storage (#333) 2025-07-05 20:51:42 +08:00
Crazylychee
ff73529a75 [ISSUE #330] Format code and update the doc (#334) 2025-07-05 20:50:36 +08:00
Crazylychee
706082c62f [ISSUE #331] Fix failing tests (#335) 2025-07-05 20:50:08 +08:00
129 changed files with 2675 additions and 2350 deletions

View File

@@ -6,19 +6,14 @@ This project was bootstrapped with [Create React App](https://github.com/faceboo
In the project directory, you can run: In the project directory, you can run:
### `npm start` ### `npm run start`
Runs the app in the development mode.\ Runs the app in the development mode.\
Open [http://localhost:3000](http://localhost:3000) to view it in your browser. Open [http://localhost:3003](http://localhost:3000) to view it in your browser.
The page will reload when you make changes.\ The page will reload when you make changes.\
You may also see any lint errors in the console. You may also see any lint errors in the console.
### `npm test`
Launches the test runner in the interactive watch mode.\
See the section about [running tests](https://facebook.github.io/create-react-app/docs/running-tests) for more information.
### `npm run build` ### `npm run build`
Builds the app for production to the `build` folder.\ Builds the app for production to the `build` folder.\

View File

@@ -18,12 +18,12 @@
<!DOCTYPE html> <!DOCTYPE html>
<html lang="en"> <html lang="en">
<head> <head>
<meta charset="UTF-8" /> <meta charset="UTF-8"/>
<link rel="shortcut icon" href="./favicon.ico" type="image/x-icon" /> <link rel="shortcut icon" href="./favicon.ico" type="image/x-icon"/>
<meta name="viewport" content="width=device-width, initial-scale=1.0" /> <meta name="viewport" content="width=device-width, initial-scale=1.0"/>
<meta name="description" content="" /> <meta name="description" content=""/>
<meta name="keywords" content="" /> <meta name="keywords" content=""/>
<meta http-equiv="X-UA-Compatible" content="IE=edge" /> <meta http-equiv="X-UA-Compatible" content="IE=edge"/>
<title>RocketMQ Dashboard</title> <title>RocketMQ Dashboard</title>
</head> </head>
<body> <body>

View File

@@ -17,7 +17,7 @@
import React from 'react'; import React from 'react';
import AppRouter from './router'; // 你 router/index.jsx 导出的组件 import AppRouter from './router'; // 你 router/index.jsx 导出的组件
import { ToastContainer } from 'react-toastify'; import {ToastContainer} from 'react-toastify';
import 'react-toastify/dist/ReactToastify.css'; import 'react-toastify/dist/ReactToastify.css';
import {ConfigProvider} from "antd"; import {ConfigProvider} from "antd";
import {useTheme} from "./store/context/ThemeContext"; import {useTheme} from "./store/context/ThemeContext";
@@ -28,8 +28,8 @@ function App() {
return ( return (
<> <>
<ConfigProvider theme={currentTheme}> <ConfigProvider theme={currentTheme}>
<ToastContainer /> <ToastContainer/>
<AppRouter /> <AppRouter/>
</ConfigProvider> </ConfigProvider>
</> </>
); );

View File

@@ -15,11 +15,11 @@
* limitations under the License. * limitations under the License.
*/ */
import { render, screen } from '@testing-library/react'; import {render, screen} from '@testing-library/react';
import App from './App'; import App from './App';
test('renders learn react link', () => { test('renders learn react link', () => {
render(<App />); render(<App/>);
const linkElement = screen.getByText(/learn react/i); const linkElement = screen.getByText(/learn react/i);
expect(linkElement).toBeInTheDocument(); expect(linkElement).toBeInTheDocument();
}); });

View File

@@ -49,7 +49,7 @@ const remoteApi = {
if (_redirectHandler) { if (_redirectHandler) {
_redirectHandler(); // 如果设置了重定向处理函数,则调用它 _redirectHandler(); // 如果设置了重定向处理函数,则调用它
} }
return { __isRedirectHandled: true }; return {__isRedirectHandled: true};
} }
return response; return response;
@@ -77,24 +77,24 @@ const remoteApi = {
listUsers: async (brokerAddress) => { listUsers: async (brokerAddress) => {
const params = new URLSearchParams(); const params = new URLSearchParams();
if (brokerAddress) params.append('brokerAddress', brokerAddress); if (brokerAddress) params.append('brokerAddress', brokerAddress);
const response = await remoteApi._fetch(remoteApi.buildUrl(`/acl/listUsers?${params.toString()}`)); const response = await remoteApi._fetch(remoteApi.buildUrl(`/acl/acls.query?${params.toString()}`));
return await response.json(); return await response.json();
}, },
createUser: async (brokerAddress, userInfo) => { createUser: async (brokerAddress, userInfo) => {
const response = await remoteApi._fetch(remoteApi.buildUrl('/acl/createUser'), { const response = await remoteApi._fetch(remoteApi.buildUrl('/acl/createUser.do'), {
method: 'POST', method: 'POST',
headers: { 'Content-Type': 'application/json' }, headers: {'Content-Type': 'application/json'},
body: JSON.stringify({ brokerAddress, userInfo }) body: JSON.stringify({brokerAddress, userInfo})
}); });
return await response.json(); // 返回字符串消息 return await response.json(); // 返回字符串消息
}, },
updateUser: async (brokerAddress, userInfo) => { updateUser: async (brokerAddress, userInfo) => {
const response = await remoteApi._fetch(remoteApi.buildUrl('/acl/updateUser'), { const response = await remoteApi._fetch(remoteApi.buildUrl('/acl/updateUser.do'), {
method: 'POST', method: 'POST',
headers: { 'Content-Type': 'application/json' }, headers: {'Content-Type': 'application/json'},
body: JSON.stringify({ brokerAddress, userInfo }) body: JSON.stringify({brokerAddress, userInfo})
}); });
return await response.json(); return await response.json();
}, },
@@ -103,7 +103,7 @@ const remoteApi = {
const params = new URLSearchParams(); const params = new URLSearchParams();
if (brokerAddress) params.append('brokerAddress', brokerAddress); if (brokerAddress) params.append('brokerAddress', brokerAddress);
params.append('username', username); params.append('username', username);
const response = await remoteApi._fetch(remoteApi.buildUrl(`/acl/deleteUser?${params.toString()}`), { const response = await remoteApi._fetch(remoteApi.buildUrl(`/acl/deleteUser.do?${params.toString()}`), {
method: 'DELETE' method: 'DELETE'
}); });
return await response.json(); return await response.json();
@@ -114,24 +114,24 @@ const remoteApi = {
const params = new URLSearchParams(); const params = new URLSearchParams();
if (brokerAddress) params.append('brokerAddress', brokerAddress); if (brokerAddress) params.append('brokerAddress', brokerAddress);
if (searchParam) params.append('searchParam', searchParam); if (searchParam) params.append('searchParam', searchParam);
const response = await remoteApi._fetch(remoteApi.buildUrl(`/acl/listAcls?${params.toString()}`)); const response = await remoteApi._fetch(remoteApi.buildUrl(`/acl/acls.query?${params.toString()}`));
return await response.json(); return await response.json();
}, },
createAcl: async (brokerAddress, subject, policies) => { createAcl: async (brokerAddress, subject, policies) => {
const response = await remoteApi._fetch(remoteApi.buildUrl('/acl/createAcl'), { const response = await remoteApi._fetch(remoteApi.buildUrl('/acl/createAcl.do'), {
method: 'POST', method: 'POST',
headers: { 'Content-Type': 'application/json' }, headers: {'Content-Type': 'application/json'},
body: JSON.stringify({ brokerAddress, subject, policies }) body: JSON.stringify({brokerAddress, subject, policies})
}); });
return await response.json(); return await response.json();
}, },
updateAcl: async (brokerAddress, subject, policies) => { updateAcl: async (brokerAddress, subject, policies) => {
const response = await remoteApi._fetch(remoteApi.buildUrl('/acl/updateAcl'), { const response = await remoteApi._fetch(remoteApi.buildUrl('/acl/updateAcl.do'), {
method: 'POST', method: 'POST',
headers: { 'Content-Type': 'application/json' }, headers: {'Content-Type': 'application/json'},
body: JSON.stringify({ brokerAddress, subject, policies }) body: JSON.stringify({brokerAddress, subject, policies})
}); });
return await response.json(); return await response.json();
}, },
@@ -141,7 +141,7 @@ const remoteApi = {
if (brokerAddress) params.append('brokerAddress', brokerAddress); if (brokerAddress) params.append('brokerAddress', brokerAddress);
params.append('subject', subject); params.append('subject', subject);
if (resource) params.append('resource', resource); if (resource) params.append('resource', resource);
const response = await remoteApi._fetch(remoteApi.buildUrl(`/acl/deleteAcl?${params.toString()}`), { const response = await remoteApi._fetch(remoteApi.buildUrl(`/acl/deleteAcl.do?${params.toString()}`), {
method: 'DELETE' method: 'DELETE'
}); });
return await response.json(); return await response.json();
@@ -159,7 +159,7 @@ const remoteApi = {
return data return data
} catch (error) { } catch (error) {
console.error("Error querying message by ID:", error); console.error("Error querying message by ID:", error);
callback({ status: 1, errMsg: "Failed to query message by ID" }); callback({status: 1, errMsg: "Failed to query message by ID"});
} }
}, },
@@ -197,7 +197,7 @@ const remoteApi = {
return data; return data;
} catch (error) { } catch (error) {
console.error("Error querying DLQ messages by consumer group:", error); console.error("Error querying DLQ messages by consumer group:", error);
return { status: 1, errMsg: "Failed to query DLQ messages by consumer group" }; return {status: 1, errMsg: "Failed to query DLQ messages by consumer group"};
} }
}, },
resendDlqMessage: async (msgId, consumerGroup, topic) => { resendDlqMessage: async (msgId, consumerGroup, topic) => {
@@ -217,7 +217,7 @@ const remoteApi = {
return data; return data;
} catch (error) { } catch (error) {
console.error("Error resending DLQ message:", error); console.error("Error resending DLQ message:", error);
return { status: 1, errMsg: "Failed to resend DLQ message" }; return {status: 1, errMsg: "Failed to resend DLQ message"};
} }
}, },
exportDlqMessage: async (msgId, consumerGroup) => { exportDlqMessage: async (msgId, consumerGroup) => {
@@ -236,7 +236,7 @@ const remoteApi = {
if (!newWindow) { if (!newWindow) {
// 浏览器可能会阻止弹窗,需要用户允许 // 浏览器可能会阻止弹窗,需要用户允许
return { status: 1, errMsg: "Failed to open new window. Please allow pop-ups for this site." }; return {status: 1, errMsg: "Failed to open new window. Please allow pop-ups for this site."};
} }
// 2. 将 JSON 数据格式化后写入新窗口 // 2. 将 JSON 数据格式化后写入新窗口
@@ -247,10 +247,10 @@ const remoteApi = {
newWindow.document.write('</body></html>'); newWindow.document.write('</body></html>');
newWindow.document.close(); // 关闭文档流,确保内容显示 newWindow.document.close(); // 关闭文档流,确保内容显示
return { status: 0, msg: "导出请求成功,内容已在新页面显示" }; return {status: 0, msg: "导出请求成功,内容已在新页面显示"};
} catch (error) { } catch (error) {
console.error("Error exporting DLQ message:", error); console.error("Error exporting DLQ message:", error);
return { status: 1, errMsg: "Failed to export DLQ message: " + error.message }; return {status: 1, errMsg: "Failed to export DLQ message: " + error.message};
} }
}, },
@@ -267,7 +267,7 @@ const remoteApi = {
return data; return data;
} catch (error) { } catch (error) {
console.error("Error batch resending DLQ messages:", error); console.error("Error batch resending DLQ messages:", error);
return { status: 1, errMsg: "Failed to batch resend DLQ messages" }; return {status: 1, errMsg: "Failed to batch resend DLQ messages"};
} }
}, },
@@ -372,18 +372,18 @@ const remoteApi = {
callback(data); callback(data);
} catch (error) { } catch (error) {
console.error("Error fetching producer connection list:", error); console.error("Error fetching producer connection list:", error);
callback({ status: 1, errMsg: "Failed to fetch producer connection list" }); // Simulate error response callback({status: 1, errMsg: "Failed to fetch producer connection list"}); // Simulate error response
} }
}, },
queryConsumerGroupList: async (skipSysGroup = false) => { queryConsumerGroupList: async (skipSysGroup, address) => {
try { try {
const response = await remoteApi._fetch(remoteApi.buildUrl(`/consumer/groupList.query?skipSysGroup=${skipSysGroup}`)); const response = await remoteApi._fetch(remoteApi.buildUrl(`/consumer/groupList.query?skipSysGroup=${skipSysGroup}&address=${address}`));
const data = await response.json(); const data = await response.json();
return data; return data;
} catch (error) { } catch (error) {
console.error("Error fetching consumer group list:", error); console.error("Error fetching consumer group list:", error);
return { status: 1, errMsg: "Failed to fetch consumer group list" }; return {status: 1, errMsg: "Failed to fetch consumer group list"};
} }
}, },
@@ -394,7 +394,7 @@ const remoteApi = {
return data; return data;
} catch (error) { } catch (error) {
console.error(`Error refreshing consumer group ${consumerGroup}:`, error); console.error(`Error refreshing consumer group ${consumerGroup}:`, error);
return { status: 1, errMsg: `Failed to refresh consumer group ${consumerGroup}` }; return {status: 1, errMsg: `Failed to refresh consumer group ${consumerGroup}`};
} }
}, },
@@ -405,7 +405,7 @@ const remoteApi = {
return data; return data;
} catch (error) { } catch (error) {
console.error("Error refreshing all consumer groups:", error); console.error("Error refreshing all consumer groups:", error);
return { status: 1, errMsg: "Failed to refresh all consumer groups" }; return {status: 1, errMsg: "Failed to refresh all consumer groups"};
} }
}, },
@@ -416,7 +416,7 @@ const remoteApi = {
return data; return data;
} catch (error) { } catch (error) {
console.error(`Error fetching monitor config for ${consumeGroupName}:`, error); console.error(`Error fetching monitor config for ${consumeGroupName}:`, error);
return { status: 1, errMsg: `Failed to fetch monitor config for ${consumeGroupName}` }; return {status: 1, errMsg: `Failed to fetch monitor config for ${consumeGroupName}`};
} }
}, },
@@ -428,13 +428,13 @@ const remoteApi = {
headers: { headers: {
'Content-Type': 'application/json' 'Content-Type': 'application/json'
}, },
body: JSON.stringify({ consumeGroupName, minCount, maxDiffTotal }) body: JSON.stringify({consumeGroupName, minCount, maxDiffTotal})
}); });
const data = await response.json(); const data = await response.json();
return data; return data;
} catch (error) { } catch (error) {
console.error("Error creating or updating consumer monitor:", error); console.error("Error creating or updating consumer monitor:", error);
return { status: 1, errMsg: "Failed to create or update consumer monitor" }; return {status: 1, errMsg: "Failed to create or update consumer monitor"};
} }
}, },
@@ -445,7 +445,7 @@ const remoteApi = {
return data; return data;
} catch (error) { } catch (error) {
console.error(`Error fetching broker name list for ${consumerGroup}:`, error); console.error(`Error fetching broker name list for ${consumerGroup}:`, error);
return { status: 1, errMsg: `Failed to fetch broker name list for ${consumerGroup}` }; return {status: 1, errMsg: `Failed to fetch broker name list for ${consumerGroup}`};
} }
}, },
@@ -456,13 +456,13 @@ const remoteApi = {
headers: { headers: {
'Content-Type': 'application/json' 'Content-Type': 'application/json'
}, },
body: JSON.stringify({ groupName, brokerNameList }) body: JSON.stringify({groupName, brokerNameList})
}); });
const data = await response.json(); const data = await response.json();
return data; return data;
} catch (error) { } catch (error) {
console.error(`Error deleting consumer group ${groupName}:`, error); console.error(`Error deleting consumer group ${groupName}:`, error);
return { status: 1, errMsg: `Failed to delete consumer group ${groupName}` }; return {status: 1, errMsg: `Failed to delete consumer group ${groupName}`};
} }
}, },
@@ -473,7 +473,7 @@ const remoteApi = {
return data; return data;
} catch (error) { } catch (error) {
console.error(`Error fetching consumer config for ${consumerGroup}:`, error); console.error(`Error fetching consumer config for ${consumerGroup}:`, error);
return { status: 1, errMsg: `Failed to fetch consumer config for ${consumerGroup}` }; return {status: 1, errMsg: `Failed to fetch consumer config for ${consumerGroup}`};
} }
}, },
@@ -490,7 +490,7 @@ const remoteApi = {
return data; return data;
} catch (error) { } catch (error) {
console.error("Error creating or updating consumer:", error); console.error("Error creating or updating consumer:", error);
return { status: 1, errMsg: "Failed to create or update consumer" }; return {status: 1, errMsg: "Failed to create or update consumer"};
} }
}, },
@@ -501,7 +501,7 @@ const remoteApi = {
return data; return data;
} catch (error) { } catch (error) {
console.error(`Error fetching topics for consumer group ${consumerGroup}:`, error); console.error(`Error fetching topics for consumer group ${consumerGroup}:`, error);
return { status: 1, errMsg: `Failed to fetch topics for consumer group ${consumerGroup}` }; return {status: 1, errMsg: `Failed to fetch topics for consumer group ${consumerGroup}`};
} }
}, },
@@ -512,7 +512,7 @@ const remoteApi = {
return data; return data;
} catch (error) { } catch (error) {
console.error(`Error fetching consumer connections for ${consumerGroup}:`, error); console.error(`Error fetching consumer connections for ${consumerGroup}:`, error);
return { status: 1, errMsg: `Failed to fetch consumer connections for ${consumerGroup}` }; return {status: 1, errMsg: `Failed to fetch consumer connections for ${consumerGroup}`};
} }
}, },
@@ -523,7 +523,7 @@ const remoteApi = {
return data; return data;
} catch (error) { } catch (error) {
console.error(`Error fetching running info for client ${clientId} in group ${consumerGroup}:`, error); console.error(`Error fetching running info for client ${clientId} in group ${consumerGroup}:`, error);
return { status: 1, errMsg: `Failed to fetch running info for client ${clientId} in group ${consumerGroup}` }; return {status: 1, errMsg: `Failed to fetch running info for client ${clientId} in group ${consumerGroup}`};
} }
}, },
queryTopicList: async () => { queryTopicList: async () => {
@@ -532,7 +532,7 @@ const remoteApi = {
return await response.json(); return await response.json();
} catch (error) { } catch (error) {
console.error("Error fetching topic list:", error); console.error("Error fetching topic list:", error);
return { status: 1, errMsg: "Failed to fetch topic list" }; return {status: 1, errMsg: "Failed to fetch topic list"};
} }
}, },
@@ -549,7 +549,7 @@ const remoteApi = {
return await response.json(); return await response.json();
} catch (error) { } catch (error) {
console.error("Error deleting topic:", error); console.error("Error deleting topic:", error);
return { status: 1, errMsg: "Failed to delete topic" }; return {status: 1, errMsg: "Failed to delete topic"};
} }
}, },
@@ -559,7 +559,7 @@ const remoteApi = {
return await response.json(); return await response.json();
} catch (error) { } catch (error) {
console.error("Error fetching topic stats:", error); console.error("Error fetching topic stats:", error);
return { status: 1, errMsg: "Failed to fetch topic stats" }; return {status: 1, errMsg: "Failed to fetch topic stats"};
} }
}, },
@@ -569,7 +569,7 @@ const remoteApi = {
return await response.json(); return await response.json();
} catch (error) { } catch (error) {
console.error("Error fetching topic route:", error); console.error("Error fetching topic route:", error);
return { status: 1, errMsg: "Failed to fetch topic route" }; return {status: 1, errMsg: "Failed to fetch topic route"};
} }
}, },
@@ -579,7 +579,7 @@ const remoteApi = {
return await response.json(); return await response.json();
} catch (error) { } catch (error) {
console.error("Error fetching topic consumers:", error); console.error("Error fetching topic consumers:", error);
return { status: 1, errMsg: "Failed to fetch topic consumers" }; return {status: 1, errMsg: "Failed to fetch topic consumers"};
} }
}, },
@@ -589,7 +589,7 @@ const remoteApi = {
return await response.json(); return await response.json();
} catch (error) { } catch (error) {
console.error("Error fetching consumer groups:", error); console.error("Error fetching consumer groups:", error);
return { status: 1, errMsg: "Failed to fetch consumer groups" }; return {status: 1, errMsg: "Failed to fetch consumer groups"};
} }
}, },
@@ -599,7 +599,7 @@ const remoteApi = {
return await response.json(); return await response.json();
} catch (error) { } catch (error) {
console.error("Error fetching topic config:", error); console.error("Error fetching topic config:", error);
return { status: 1, errMsg: "Failed to fetch topic config" }; return {status: 1, errMsg: "Failed to fetch topic config"};
} }
}, },
@@ -609,7 +609,7 @@ const remoteApi = {
return await response.json(); return await response.json();
} catch (error) { } catch (error) {
console.error("Error fetching cluster list:", error); console.error("Error fetching cluster list:", error);
return { status: 1, errMsg: "Failed to fetch cluster list" }; return {status: 1, errMsg: "Failed to fetch cluster list"};
} }
}, },
@@ -625,7 +625,7 @@ const remoteApi = {
return await response.json(); return await response.json();
} catch (error) { } catch (error) {
console.error("Error creating/updating topic:", error); console.error("Error creating/updating topic:", error);
return { status: 1, errMsg: "Failed to create/update topic" }; return {status: 1, errMsg: "Failed to create/update topic"};
} }
}, },
@@ -641,7 +641,7 @@ const remoteApi = {
return await response.json(); return await response.json();
} catch (error) { } catch (error) {
console.error("Error resetting consumer offset:", error); console.error("Error resetting consumer offset:", error);
return { status: 1, errMsg: "Failed to reset consumer offset" }; return {status: 1, errMsg: "Failed to reset consumer offset"};
} }
}, },
@@ -657,7 +657,7 @@ const remoteApi = {
return await response.json(); return await response.json();
} catch (error) { } catch (error) {
console.error("Error skipping message accumulate:", error); console.error("Error skipping message accumulate:", error);
return { status: 1, errMsg: "Failed to skip message accumulate" }; return {status: 1, errMsg: "Failed to skip message accumulate"};
} }
}, },
@@ -673,7 +673,7 @@ const remoteApi = {
return await response.json(); return await response.json();
} catch (error) { } catch (error) {
console.error("Error sending topic message:", error); console.error("Error sending topic message:", error);
return { status: 1, errMsg: "Failed to send topic message" }; return {status: 1, errMsg: "Failed to send topic message"};
} }
}, },
@@ -684,12 +684,12 @@ const remoteApi = {
headers: { headers: {
'Content-Type': 'application/json', 'Content-Type': 'application/json',
}, },
body: JSON.stringify({ brokerName, topic }) body: JSON.stringify({brokerName, topic})
}); });
return await response.json(); return await response.json();
} catch (error) { } catch (error) {
console.error("Error deleting topic by broker:", error); console.error("Error deleting topic by broker:", error);
return { status: 1, errMsg: "Failed to delete topic by broker" }; return {status: 1, errMsg: "Failed to delete topic by broker"};
} }
}, },
@@ -700,7 +700,7 @@ const remoteApi = {
return await response.json(); return await response.json();
} catch (error) { } catch (error) {
console.error("Error fetching ops home page data:", error); console.error("Error fetching ops home page data:", error);
return { status: 1, errMsg: "Failed to fetch ops home page data" }; return {status: 1, errMsg: "Failed to fetch ops home page data"};
} }
}, },
@@ -712,7 +712,7 @@ const remoteApi = {
return await response.json(); return await response.json();
} catch (error) { } catch (error) {
console.error("Error updating NameServer address:", error); console.error("Error updating NameServer address:", error);
return { status: 1, errMsg: "Failed to update NameServer address" }; return {status: 1, errMsg: "Failed to update NameServer address"};
} }
}, },
@@ -724,7 +724,7 @@ const remoteApi = {
return await response.json(); return await response.json();
} catch (error) { } catch (error) {
console.error("Error adding NameServer address:", error); console.error("Error adding NameServer address:", error);
return { status: 1, errMsg: "Failed to add NameServer address" }; return {status: 1, errMsg: "Failed to add NameServer address"};
} }
}, },
@@ -736,7 +736,7 @@ const remoteApi = {
return await response.json(); return await response.json();
} catch (error) { } catch (error) {
console.error("Error updating VIP Channel status:", error); console.error("Error updating VIP Channel status:", error);
return { status: 1, errMsg: "Failed to update VIP Channel status" }; return {status: 1, errMsg: "Failed to update VIP Channel status"};
} }
}, },
@@ -748,7 +748,7 @@ const remoteApi = {
return await response.json(); return await response.json();
} catch (error) { } catch (error) {
console.error("Error updating TLS status:", error); console.error("Error updating TLS status:", error);
return { status: 1, errMsg: "Failed to update TLS status" }; return {status: 1, errMsg: "Failed to update TLS status"};
} }
}, },
@@ -759,7 +759,7 @@ const remoteApi = {
callback(data); callback(data);
} catch (error) { } catch (error) {
console.error("Error fetching cluster list:", error); console.error("Error fetching cluster list:", error);
callback({ status: 1, errMsg: "Failed to fetch cluster list" }); callback({status: 1, errMsg: "Failed to fetch cluster list"});
} }
}, },
@@ -767,16 +767,16 @@ const remoteApi = {
try { try {
const url = new URL(remoteApi.buildUrl('/dashboard/broker.query')); const url = new URL(remoteApi.buildUrl('/dashboard/broker.query'));
url.searchParams.append('date', date); url.searchParams.append('date', date);
const response = await remoteApi._fetch(url.toString(), { signal: AbortSignal.timeout(15000) }); // 15s timeout const response = await remoteApi._fetch(url.toString(), {signal: AbortSignal.timeout(15000)}); // 15s timeout
const data = await response.json(); const data = await response.json();
callback(data); callback(data);
} catch (error) { } catch (error) {
if (error.name === 'TimeoutError') { if (error.name === 'TimeoutError') {
console.error("Broker history data request timed out:", error); console.error("Broker history data request timed out:", error);
callback({ status: 1, errMsg: "Request timed out for broker history data" }); callback({status: 1, errMsg: "Request timed out for broker history data"});
} else { } else {
console.error("Error fetching broker history data:", error); console.error("Error fetching broker history data:", error);
callback({ status: 1, errMsg: "Failed to fetch broker history data" }); callback({status: 1, errMsg: "Failed to fetch broker history data"});
} }
} }
}, },
@@ -786,32 +786,32 @@ const remoteApi = {
const url = new URL(remoteApi.buildUrl('/dashboard/topic.query')); const url = new URL(remoteApi.buildUrl('/dashboard/topic.query'));
url.searchParams.append('date', date); url.searchParams.append('date', date);
url.searchParams.append('topicName', topicName); url.searchParams.append('topicName', topicName);
const response = await remoteApi._fetch(url.toString(), { signal: AbortSignal.timeout(15000) }); // 15s timeout const response = await remoteApi._fetch(url.toString(), {signal: AbortSignal.timeout(15000)}); // 15s timeout
const data = await response.json(); const data = await response.json();
callback(data); callback(data);
} catch (error) { } catch (error) {
if (error.name === 'TimeoutError') { if (error.name === 'TimeoutError') {
console.error("Topic history data request timed out:", error); console.error("Topic history data request timed out:", error);
callback({ status: 1, errMsg: "Request timed out for topic history data" }); callback({status: 1, errMsg: "Request timed out for topic history data"});
} else { } else {
console.error("Error fetching topic history data:", error); console.error("Error fetching topic history data:", error);
callback({ status: 1, errMsg: "Failed to fetch topic history data" }); callback({status: 1, errMsg: "Failed to fetch topic history data"});
} }
} }
}, },
queryTopicCurrentData: async (callback) => { queryTopicCurrentData: async (callback) => {
try { try {
const response = await remoteApi._fetch(remoteApi.buildUrl('/dashboard/topicCurrent.query'), { signal: AbortSignal.timeout(15000) }); // 15s timeout const response = await remoteApi._fetch(remoteApi.buildUrl('/dashboard/topicCurrent.query'), {signal: AbortSignal.timeout(15000)}); // 15s timeout
const data = await response.json(); const data = await response.json();
callback(data); callback(data);
} catch (error) { } catch (error) {
if (error.name === 'TimeoutError') { if (error.name === 'TimeoutError') {
console.error("Topic current data request timed out:", error); console.error("Topic current data request timed out:", error);
callback({ status: 1, errMsg: "Request timed out for topic current data" }); callback({status: 1, errMsg: "Request timed out for topic current data"});
} else { } else {
console.error("Error fetching topic current data:", error); console.error("Error fetching topic current data:", error);
callback({ status: 1, errMsg: "Failed to fetch topic current data" }); callback({status: 1, errMsg: "Failed to fetch topic current data"});
} }
} }
}, },
@@ -825,7 +825,7 @@ const remoteApi = {
callback(data); callback(data);
} catch (error) { } catch (error) {
console.error("Error fetching broker config:", error); console.error("Error fetching broker config:", error);
callback({ status: 1, errMsg: "Failed to fetch broker config" }); callback({status: 1, errMsg: "Failed to fetch broker config"});
} }
}, },
@@ -839,7 +839,7 @@ const remoteApi = {
callback(data); callback(data);
} catch (error) { } catch (error) {
console.error("Error fetching proxy home page:", error); console.error("Error fetching proxy home page:", error);
callback({ status: 1, errMsg: "Failed to fetch proxy home page" }); callback({status: 1, errMsg: "Failed to fetch proxy home page"});
} }
}, },
@@ -850,14 +850,14 @@ const remoteApi = {
try { try {
const response = await remoteApi._fetch(remoteApi.buildUrl("/proxy/addProxyAddr.do"), { const response = await remoteApi._fetch(remoteApi.buildUrl("/proxy/addProxyAddr.do"), {
method: 'POST', method: 'POST',
headers: { 'Content-Type': 'application/x-www-form-urlencoded' }, headers: {'Content-Type': 'application/x-www-form-urlencoded'},
body: new URLSearchParams({ newProxyAddr }).toString() body: new URLSearchParams({newProxyAddr}).toString()
}); });
const data = await response.json(); const data = await response.json();
callback(data); callback(data);
} catch (error) { } catch (error) {
console.error("Error adding proxy address:", error); console.error("Error adding proxy address:", error);
callback({ status: 1, errMsg: "Failed to add proxy address" }); callback({status: 1, errMsg: "Failed to add proxy address"});
} }
}, },
login: async (username, password) => { login: async (username, password) => {
@@ -882,19 +882,19 @@ const remoteApi = {
return data; return data;
} catch (error) { } catch (error) {
console.error("Error logging in:", error); console.error("Error logging in:", error);
return { status: 1, errMsg: "Failed to log in" }; return {status: 1, errMsg: "Failed to log in"};
} }
}, },
logout: async () => { logout: async () => {
try { try {
const response = await remoteApi._fetch(remoteApi.buildUrl("/login/logout.do"),{ const response = await remoteApi._fetch(remoteApi.buildUrl("/login/logout.do"), {
method: 'POST' method: 'POST'
}); });
return await response.json() return await response.json()
}catch (error) { } catch (error) {
console.error("Error logging out:", error); console.error("Error logging out:", error);
return { status: 1, errMsg: "Failed to log out" }; return {status: 1, errMsg: "Failed to log out"};
} }
} }
}; };
@@ -939,4 +939,4 @@ const tools = {
} }
}; };
export { remoteApi, tools }; export {remoteApi, tools};

View File

@@ -16,20 +16,20 @@
*/ */
import React from 'react'; import React from 'react';
import { Form, Input, Typography, Modal } from 'antd'; import {Form, Input, Typography} from 'antd';
import moment from 'moment'; import moment from 'moment';
import { useLanguage } from '../i18n/LanguageContext'; // 根据实际路径调整 import {useLanguage} from '../i18n/LanguageContext'; // 根据实际路径调整
const { Text } = Typography; const {Text} = Typography;
const DlqMessageDetailViewDialog = ({ ngDialogData }) => { const DlqMessageDetailViewDialog = ({ngDialogData}) => {
const { t } = useLanguage(); const {t} = useLanguage();
const messageView = ngDialogData?.messageView || {}; const messageView = ngDialogData?.messageView || {};
return ( return (
<div style={{ padding: '20px' }}> <div style={{padding: '20px'}}>
<Form layout="horizontal" labelCol={{ span: 4 }} wrapperCol={{ span: 20 }}> <Form layout="horizontal" labelCol={{span: 4}} wrapperCol={{span: 20}}>
<Form.Item label="Message ID:"> <Form.Item label="Message ID:">
<Text strong>{messageView.msgId}</Text> <Text strong>{messageView.msgId}</Text>
</Form.Item> </Form.Item>
@@ -39,7 +39,7 @@ const DlqMessageDetailViewDialog = ({ ngDialogData }) => {
<Form.Item label="Properties:"> <Form.Item label="Properties:">
<Input.TextArea <Input.TextArea
value={typeof messageView.properties === 'object' ? JSON.stringify(messageView.properties, null, 2) : messageView.properties} value={typeof messageView.properties === 'object' ? JSON.stringify(messageView.properties, null, 2) : messageView.properties}
style={{ minHeight: 100, resize: 'none' }} style={{minHeight: 100, resize: 'none'}}
readOnly readOnly
/> />
</Form.Item> </Form.Item>
@@ -61,7 +61,7 @@ const DlqMessageDetailViewDialog = ({ ngDialogData }) => {
<Form.Item label="Message body:"> <Form.Item label="Message body:">
<Input.TextArea <Input.TextArea
value={messageView.messageBody} value={messageView.messageBody}
style={{ minHeight: 100, resize: 'none' }} style={{minHeight: 100, resize: 'none'}}
readOnly readOnly
/> />
</Form.Item> </Form.Item>

View File

@@ -16,16 +16,16 @@
*/ */
import React from 'react'; import React from 'react';
import { Modal, Button, Typography, Descriptions, Tag, Spin, notification } from 'antd'; import {Button, Descriptions, Modal, notification, Spin, Tag, Typography} from 'antd';
import moment from 'moment'; import moment from 'moment';
import { ExclamationCircleOutlined, SyncOutlined } from '@ant-design/icons'; import {SyncOutlined} from '@ant-design/icons';
import { useLanguage } from '../i18n/LanguageContext'; import {useLanguage} from '../i18n/LanguageContext';
import { remoteApi } from '../api/remoteApi/remoteApi'; // 确保这个路径正确 import {remoteApi} from '../api/remoteApi/remoteApi'; // 确保这个路径正确
const { Text, Paragraph } = Typography; const {Text, Paragraph} = Typography;
const MessageDetailViewDialog = ({ visible, onCancel, messageId, topic, onResendMessage }) => { const MessageDetailViewDialog = ({visible, onCancel, messageId, topic, onResendMessage}) => {
const { t } = useLanguage(); const {t} = useLanguage();
const [loading, setLoading] = React.useState(true); const [loading, setLoading] = React.useState(true);
const [messageDetail, setMessageDetail] = React.useState(null); const [messageDetail, setMessageDetail] = React.useState(null);
const [error, setError] = React.useState(null); const [error, setError] = React.useState(null);
@@ -89,17 +89,21 @@ const MessageDetailViewDialog = ({ visible, onCancel, messageId, topic, onResend
> >
<Spin spinning={loading} tip={t.LOADING}> <Spin spinning={loading} tip={t.LOADING}>
{error && ( {error && (
<Paragraph type="danger" style={{ textAlign: 'center' }}> <Paragraph type="danger" style={{textAlign: 'center'}}>
{error} {error}
</Paragraph> </Paragraph>
)} )}
{messageDetail ? ( // 确保 messageDetail 存在时才渲染内容 {messageDetail ? ( // 确保 messageDetail 存在时才渲染内容
<> <>
{/* 消息信息部分 */} {/* 消息信息部分 */}
<Descriptions title={<Text strong>{t.MESSAGE_INFO}</Text>} bordered column={2} size="small" style={{ marginBottom: 20 }}> <Descriptions title={<Text strong>{t.MESSAGE_INFO}</Text>} bordered column={2} size="small"
<Descriptions.Item label="Topic" span={2}><Text copyable>{messageDetail.messageView.topic}</Text></Descriptions.Item> style={{marginBottom: 20}}>
<Descriptions.Item label="Message ID" span={2}><Text copyable>{messageDetail.messageView.msgId}</Text></Descriptions.Item> <Descriptions.Item label="Topic" span={2}><Text
<Descriptions.Item label="StoreHost">{messageDetail.messageView.storeHost}</Descriptions.Item> copyable>{messageDetail.messageView.topic}</Text></Descriptions.Item>
<Descriptions.Item label="Message ID" span={2}><Text
copyable>{messageDetail.messageView.msgId}</Text></Descriptions.Item>
<Descriptions.Item
label="StoreHost">{messageDetail.messageView.storeHost}</Descriptions.Item>
<Descriptions.Item label="BornHost">{messageDetail.messageView.bornHost}</Descriptions.Item> <Descriptions.Item label="BornHost">{messageDetail.messageView.bornHost}</Descriptions.Item>
<Descriptions.Item label="StoreTime"> <Descriptions.Item label="StoreTime">
{moment(messageDetail.messageView.storeTimestamp).format("YYYY-MM-DD HH:mm:ss")} {moment(messageDetail.messageView.storeTimestamp).format("YYYY-MM-DD HH:mm:ss")}
@@ -108,26 +112,33 @@ const MessageDetailViewDialog = ({ visible, onCancel, messageId, topic, onResend
{moment(messageDetail.messageView.bornTimestamp).format("YYYY-MM-DD HH:mm:ss")} {moment(messageDetail.messageView.bornTimestamp).format("YYYY-MM-DD HH:mm:ss")}
</Descriptions.Item> </Descriptions.Item>
<Descriptions.Item label="Queue ID">{messageDetail.messageView.queueId}</Descriptions.Item> <Descriptions.Item label="Queue ID">{messageDetail.messageView.queueId}</Descriptions.Item>
<Descriptions.Item label="Queue Offset">{messageDetail.messageView.queueOffset}</Descriptions.Item> <Descriptions.Item
<Descriptions.Item label="StoreSize">{messageDetail.messageView.storeSize} bytes</Descriptions.Item> label="Queue Offset">{messageDetail.messageView.queueOffset}</Descriptions.Item>
<Descriptions.Item label="ReconsumeTimes">{messageDetail.messageView.reconsumeTimes}</Descriptions.Item> <Descriptions.Item
label="StoreSize">{messageDetail.messageView.storeSize} bytes</Descriptions.Item>
<Descriptions.Item
label="ReconsumeTimes">{messageDetail.messageView.reconsumeTimes}</Descriptions.Item>
<Descriptions.Item label="BodyCRC">{messageDetail.messageView.bodyCRC}</Descriptions.Item> <Descriptions.Item label="BodyCRC">{messageDetail.messageView.bodyCRC}</Descriptions.Item>
<Descriptions.Item label="SysFlag">{messageDetail.messageView.sysFlag}</Descriptions.Item> <Descriptions.Item label="SysFlag">{messageDetail.messageView.sysFlag}</Descriptions.Item>
<Descriptions.Item label="Flag">{messageDetail.messageView.flag}</Descriptions.Item> <Descriptions.Item label="Flag">{messageDetail.messageView.flag}</Descriptions.Item>
<Descriptions.Item label="PreparedTransactionOffset">{messageDetail.messageView.preparedTransactionOffset}</Descriptions.Item> <Descriptions.Item
label="PreparedTransactionOffset">{messageDetail.messageView.preparedTransactionOffset}</Descriptions.Item>
</Descriptions> </Descriptions>
{/* 消息属性部分 */} {/* 消息属性部分 */}
{Object.keys(messageDetail.messageView.properties).length > 0 && ( {Object.keys(messageDetail.messageView.properties).length > 0 && (
<Descriptions title={<Text strong>{t.MESSAGE_PROPERTIES}</Text>} bordered column={1} size="small" style={{ marginBottom: 20 }}> <Descriptions title={<Text strong>{t.MESSAGE_PROPERTIES}</Text>} bordered column={1}
size="small" style={{marginBottom: 20}}>
{Object.entries(messageDetail.messageView.properties).map(([key, value]) => ( {Object.entries(messageDetail.messageView.properties).map(([key, value]) => (
<Descriptions.Item label={key} key={key}><Text copyable>{value}</Text></Descriptions.Item> <Descriptions.Item label={key} key={key}><Text
copyable>{value}</Text></Descriptions.Item>
))} ))}
</Descriptions> </Descriptions>
)} )}
{/* 消息体部分 */} {/* 消息体部分 */}
<Descriptions title={<Text strong>{t.MESSAGE_BODY}</Text>} bordered column={1} size="small" style={{ marginBottom: 20 }}> <Descriptions title={<Text strong>{t.MESSAGE_BODY}</Text>} bordered column={1} size="small"
style={{marginBottom: 20}}>
<Descriptions.Item> <Descriptions.Item>
<Paragraph <Paragraph
copyable copyable
@@ -146,9 +157,10 @@ const MessageDetailViewDialog = ({ visible, onCancel, messageId, topic, onResend
{messageDetail.messageTrackList && messageDetail.messageTrackList.length > 0 ? ( {messageDetail.messageTrackList && messageDetail.messageTrackList.length > 0 ? (
<> <>
<Text strong>{t.MESSAGE_TRACKING}</Text> <Text strong>{t.MESSAGE_TRACKING}</Text>
<div style={{ marginTop: 10 }}> <div style={{marginTop: 10}}>
{messageDetail.messageTrackList.map((track, index) => ( {messageDetail.messageTrackList.map((track, index) => (
<Descriptions bordered column={1} size="small" key={index} style={{ marginBottom: 15 }}> <Descriptions bordered column={1} size="small" key={index}
style={{marginBottom: 15}}>
<Descriptions.Item label={t.CONSUMER_GROUP}> <Descriptions.Item label={t.CONSUMER_GROUP}>
{track.consumerGroup} {track.consumerGroup}
</Descriptions.Item> </Descriptions.Item>
@@ -165,10 +177,10 @@ const MessageDetailViewDialog = ({ visible, onCancel, messageId, topic, onResend
</Descriptions.Item> </Descriptions.Item>
<Descriptions.Item label={t.OPERATION}> <Descriptions.Item label={t.OPERATION}>
<Button <Button
icon={<SyncOutlined />} icon={<SyncOutlined/>}
onClick={() => onResendMessage(messageDetail.messageView, track.consumerGroup)} onClick={() => onResendMessage(messageDetail.messageView, track.consumerGroup)}
size="small" size="small"
style={{ marginRight: 8 }} style={{marginRight: 8}}
> >
{t.RESEND_MESSAGE} {t.RESEND_MESSAGE}
</Button> </Button>
@@ -181,7 +193,10 @@ const MessageDetailViewDialog = ({ visible, onCancel, messageId, topic, onResend
ellipsis={{ ellipsis={{
rows: 2, // 默认显示2行 rows: 2, // 默认显示2行
expandable: true, expandable: true,
symbol: <Text style={{ color: '#1890ff', cursor: 'pointer' }}>{t.READ_MORE}</Text>, // 蓝色展开文本 symbol: <Text style={{
color: '#1890ff',
cursor: 'pointer'
}}>{t.READ_MORE}</Text>, // 蓝色展开文本
}} }}
> >
{track.exceptionDesc} {track.exceptionDesc}
@@ -198,7 +213,8 @@ const MessageDetailViewDialog = ({ visible, onCancel, messageId, topic, onResend
</> </>
) : ( ) : (
// 当 messageDetail 为 null 时,可以显示一个占位符或者不显示内容 // 当 messageDetail 为 null 时,可以显示一个占位符或者不显示内容
!loading && !error && <Paragraph style={{ textAlign: 'center' }}>{t.NO_MESSAGE_DETAIL_AVAILABLE}</Paragraph> !loading && !error &&
<Paragraph style={{textAlign: 'center'}}>{t.NO_MESSAGE_DETAIL_AVAILABLE}</Paragraph>
)} )}
</Spin> </Spin>
</Modal> </Modal>

View File

@@ -15,15 +15,15 @@
* limitations under the License. * limitations under the License.
*/ */
import React, { useEffect, useRef } from 'react'; import React, {useEffect, useRef} from 'react';
import { Form, Input, Typography, Collapse, Table, Tag } from 'antd'; import {Collapse, Form, Input, Table, Tag, Typography} from 'antd';
import moment from 'moment'; import moment from 'moment';
import { useLanguage } from '../i18n/LanguageContext'; import {useLanguage} from '../i18n/LanguageContext';
import Paragraph from "antd/es/skeleton/Paragraph"; import Paragraph from "antd/es/skeleton/Paragraph";
import * as echarts from 'echarts'; // Import ECharts import * as echarts from 'echarts'; // Import ECharts
const { Text } = Typography; const {Text} = Typography;
const { Panel } = Collapse; const {Panel} = Collapse;
// Constants for styling and formatting, derived from the example // Constants for styling and formatting, derived from the example
const SUCCESS_COLOR = '#75d874'; const SUCCESS_COLOR = '#75d874';
@@ -36,8 +36,8 @@ const TIME_FORMAT_PATTERN = "YYYY-MM-DD HH:mm:ss.SSS";
const DEFAULT_DISPLAY_DURATION = 10 * 1000; const DEFAULT_DISPLAY_DURATION = 10 * 1000;
const TRANSACTION_CHECK_COST_TIME = 50; // transactionTraceNode do not have costTime, assume it cost 50ms const TRANSACTION_CHECK_COST_TIME = 50; // transactionTraceNode do not have costTime, assume it cost 50ms
const MessageTraceDetailViewDialog = ({ ngDialogData }) => { const MessageTraceDetailViewDialog = ({ngDialogData}) => {
const { t } = useLanguage(); const {t} = useLanguage();
const messageTraceGraphRef = useRef(null); const messageTraceGraphRef = useRef(null);
const producerNode = ngDialogData?.producerNode; const producerNode = ngDialogData?.producerNode;
@@ -125,6 +125,7 @@ const MessageTraceDetailViewDialog = ({ ngDialogData }) => {
} }
return `Cost Time: ${formatCostTimeStr(costTime)}<br/>` return `Cost Time: ${formatCostTimeStr(costTime)}<br/>`
} }
function buildTimeStamp(timestamp) { function buildTimeStamp(timestamp) {
if (timestamp < 0) { if (timestamp < 0) {
return 'N/A'; return 'N/A';
@@ -323,94 +324,181 @@ const MessageTraceDetailViewDialog = ({ ngDialogData }) => {
// ... (rest of your existing component code) // ... (rest of your existing component code)
const transactionColumns = [ const transactionColumns = [
{ title: t.TIMESTAMP, dataIndex: 'beginTimestamp', key: 'beginTimestamp', align: 'center', render: (text) => moment(text).format('YYYY-MM-DD HH:mm:ss.SSS') }, {
{ title: t.TRANSACTION_STATE, dataIndex: 'transactionState', key: 'transactionState', align: 'center', render: (text) => <Tag color={text === 'COMMIT_MESSAGE' ? 'green' : (text === 'ROLLBACK_MESSAGE' ? 'red' : 'default')}>{text}</Tag> }, title: t.TIMESTAMP,
{ title: t.FROM_TRANSACTION_CHECK, dataIndex: 'fromTransactionCheck', key: 'fromTransactionCheck', align: 'center', render: (text) => (text ? <Tag color="blue">{t.YES}</Tag> : <Tag color="purple">{t.NO}</Tag>) }, dataIndex: 'beginTimestamp',
{ title: t.CLIENT_HOST, dataIndex: 'clientHost', key: 'clientHost', align: 'center' }, key: 'beginTimestamp',
{ title: t.STORE_HOST, dataIndex: 'storeHost', key: 'storeHost', align: 'center' }, align: 'center',
render: (text) => moment(text).format('YYYY-MM-DD HH:mm:ss.SSS')
},
{
title: t.TRANSACTION_STATE,
dataIndex: 'transactionState',
key: 'transactionState',
align: 'center',
render: (text) => <Tag
color={text === 'COMMIT_MESSAGE' ? 'green' : (text === 'ROLLBACK_MESSAGE' ? 'red' : 'default')}>{text}</Tag>
},
{
title: t.FROM_TRANSACTION_CHECK,
dataIndex: 'fromTransactionCheck',
key: 'fromTransactionCheck',
align: 'center',
render: (text) => (text ? <Tag color="blue">{t.YES}</Tag> : <Tag color="purple">{t.NO}</Tag>)
},
{title: t.CLIENT_HOST, dataIndex: 'clientHost', key: 'clientHost', align: 'center'},
{title: t.STORE_HOST, dataIndex: 'storeHost', key: 'storeHost', align: 'center'},
]; ];
const consumeColumns = [ const consumeColumns = [
{ title: t.BEGIN_TIMESTAMP, dataIndex: 'beginTimestamp', key: 'beginTimestamp', align: 'center', render: (text) => text < 0 ? 'N/A' : moment(text).format('YYYY-MM-DD HH:mm:ss.SSS') }, {
{ title: t.END_TIMESTAMP, dataIndex: 'endTimestamp', key: 'endTimestamp', align: 'center', render: (text) => text < 0 ? 'N/A' : moment(text).format('YYYY-MM-DD HH:mm:ss.SSS') }, title: t.BEGIN_TIMESTAMP,
{ title: t.COST_TIME, dataIndex: 'costTime', key: 'costTime', align: 'center', render: (text) => text < 0 ? 'N/A' : `${text === 0 ? '<1' : text}ms` }, dataIndex: 'beginTimestamp',
{ title: t.STATUS, dataIndex: 'status', key: 'status', align: 'center', render: (text) => <Tag color={text === 'SUCCESS' ? 'green' : (text === 'FAILED' ? 'red' : 'default')}>{text}</Tag> }, key: 'beginTimestamp',
{ title: t.RETRY_TIMES, dataIndex: 'retryTimes', key: 'retryTimes', align: 'center', render: (text) => text < 0 ? 'N/A' : text }, align: 'center',
{ title: t.CLIENT_HOST, dataIndex: 'clientHost', key: 'clientHost', align: 'center' }, render: (text) => text < 0 ? 'N/A' : moment(text).format('YYYY-MM-DD HH:mm:ss.SSS')
{ title: t.STORE_HOST, dataIndex: 'storeHost', key: 'storeHost', align: 'center' }, },
{
title: t.END_TIMESTAMP,
dataIndex: 'endTimestamp',
key: 'endTimestamp',
align: 'center',
render: (text) => text < 0 ? 'N/A' : moment(text).format('YYYY-MM-DD HH:mm:ss.SSS')
},
{
title: t.COST_TIME,
dataIndex: 'costTime',
key: 'costTime',
align: 'center',
render: (text) => text < 0 ? 'N/A' : `${text === 0 ? '<1' : text}ms`
},
{
title: t.STATUS,
dataIndex: 'status',
key: 'status',
align: 'center',
render: (text) => <Tag
color={text === 'SUCCESS' ? 'green' : (text === 'FAILED' ? 'red' : 'default')}>{text}</Tag>
},
{
title: t.RETRY_TIMES,
dataIndex: 'retryTimes',
key: 'retryTimes',
align: 'center',
render: (text) => text < 0 ? 'N/A' : text
},
{title: t.CLIENT_HOST, dataIndex: 'clientHost', key: 'clientHost', align: 'center'},
{title: t.STORE_HOST, dataIndex: 'storeHost', key: 'storeHost', align: 'center'},
]; ];
return ( return (
<div style={{ padding: '20px', backgroundColor: '#f0f2f5' }}> <div style={{padding: '20px', backgroundColor: '#f0f2f5'}}>
<div style={{ marginBottom: '20px', borderRadius: '8px', overflow: 'hidden', boxShadow: '0 4px 12px rgba(0, 0, 0, 0.08)' }}> <div style={{
marginBottom: '20px',
borderRadius: '8px',
overflow: 'hidden',
boxShadow: '0 4px 12px rgba(0, 0, 0, 0.08)'
}}>
<Collapse defaultActiveKey={['messageTraceGraph']} expandIconPosition="right"> <Collapse defaultActiveKey={['messageTraceGraph']} expandIconPosition="right">
<Panel header={<Typography.Title level={3} style={{ margin: 0, color: '#333' }}>{t.MESSAGE_TRACE_GRAPH}</Typography.Title>} key="messageTraceGraph"> <Panel header={<Typography.Title level={3} style={{
<div ref={messageTraceGraphRef} style={{ height: 500, width: '100%', backgroundColor: '#fff', padding: '10px' }}> margin: 0,
color: '#333'
}}>{t.MESSAGE_TRACE_GRAPH}</Typography.Title>} key="messageTraceGraph">
<div ref={messageTraceGraphRef}
style={{height: 500, width: '100%', backgroundColor: '#fff', padding: '10px'}}>
{/* ECharts message trace graph will be rendered here */} {/* ECharts message trace graph will be rendered here */}
{(!producerNode && subscriptionNodeList.length === 0) && ( {(!producerNode && subscriptionNodeList.length === 0) && (
<Text type="secondary" style={{ display: 'block', textAlign: 'center', marginTop: '150px' }}>{t.TRACE_GRAPH_PLACEHOLDER}</Text> <Text type="secondary" style={{
display: 'block',
textAlign: 'center',
marginTop: '150px'
}}>{t.TRACE_GRAPH_PLACEHOLDER}</Text>
)} )}
</div> </div>
</Panel> </Panel>
</Collapse> </Collapse>
</div> </div>
<div style={{ marginBottom: '20px', borderRadius: '8px', overflow: 'hidden', boxShadow: '0 4px 12px rgba(0, 0, 0, 0.08)' }}> <div style={{
marginBottom: '20px',
borderRadius: '8px',
overflow: 'hidden',
boxShadow: '0 4px 12px rgba(0, 0, 0, 0.08)'
}}>
<Collapse defaultActiveKey={['sendMessageTrace']} expandIconPosition="right"> <Collapse defaultActiveKey={['sendMessageTrace']} expandIconPosition="right">
<Panel header={<Typography.Title level={3} style={{ margin: 0, color: '#333' }}>{t.SEND_MESSAGE_TRACE}</Typography.Title>} key="sendMessageTrace"> <Panel header={<Typography.Title level={3} style={{
margin: 0,
color: '#333'
}}>{t.SEND_MESSAGE_TRACE}</Typography.Title>} key="sendMessageTrace">
{!producerNode ? ( {!producerNode ? (
<Paragraph style={{ padding: '16px', textAlign: 'center', color: '#666' }}>{t.NO_PRODUCER_TRACE_DATA}</Paragraph> <Paragraph style={{
padding: '16px',
textAlign: 'center',
color: '#666'
}}>{t.NO_PRODUCER_TRACE_DATA}</Paragraph>
) : ( ) : (
<div style={{ padding: '16px', backgroundColor: '#fff' }}> <div style={{padding: '16px', backgroundColor: '#fff'}}>
<Typography.Title level={4} style={{ marginBottom: '20px' }}> <Typography.Title level={4} style={{marginBottom: '20px'}}>
{t.SEND_MESSAGE_INFO} : ( {t.MESSAGE_ID} <Text strong copyable>{producerNode.msgId}</Text> ) {t.SEND_MESSAGE_INFO} : ( {t.MESSAGE_ID} <Text strong
copyable>{producerNode.msgId}</Text> )
</Typography.Title> </Typography.Title>
<Form layout="vertical" colon={false}> <Form layout="vertical" colon={false}>
<div style={{ display: 'grid', gridTemplateColumns: 'repeat(auto-fill, minmax(280px, 1fr))', gap: '16px' }}> <div style={{
display: 'grid',
gridTemplateColumns: 'repeat(auto-fill, minmax(280px, 1fr))',
gap: '16px'
}}>
<Form.Item label={<Text strong>{t.TOPIC}</Text>}> <Form.Item label={<Text strong>{t.TOPIC}</Text>}>
<Input value={producerNode.topic} readOnly /> <Input value={producerNode.topic} readOnly/>
</Form.Item> </Form.Item>
<Form.Item label={<Text strong>{t.PRODUCER_GROUP}</Text>}> <Form.Item label={<Text strong>{t.PRODUCER_GROUP}</Text>}>
<Input value={producerNode.groupName} readOnly /> <Input value={producerNode.groupName} readOnly/>
</Form.Item> </Form.Item>
<Form.Item label={<Text strong>{t.MESSAGE_KEY}</Text>}> <Form.Item label={<Text strong>{t.MESSAGE_KEY}</Text>}>
<Input value={producerNode.keys} readOnly /> <Input value={producerNode.keys} readOnly/>
</Form.Item> </Form.Item>
<Form.Item label={<Text strong>{t.TAG}</Text>}> <Form.Item label={<Text strong>{t.TAG}</Text>}>
<Input value={producerNode.tags} readOnly /> <Input value={producerNode.tags} readOnly/>
</Form.Item> </Form.Item>
<Form.Item label={<Text strong>{t.BEGIN_TIMESTAMP}</Text>}> <Form.Item label={<Text strong>{t.BEGIN_TIMESTAMP}</Text>}>
<Input value={moment(producerNode.traceNode.beginTimestamp).format('YYYY-MM-DD HH:mm:ss.SSS')} readOnly /> <Input
value={moment(producerNode.traceNode.beginTimestamp).format('YYYY-MM-DD HH:mm:ss.SSS')}
readOnly/>
</Form.Item> </Form.Item>
<Form.Item label={<Text strong>{t.END_TIMESTAMP}</Text>}> <Form.Item label={<Text strong>{t.END_TIMESTAMP}</Text>}>
<Input value={moment(producerNode.traceNode.endTimestamp).format('YYYY-MM-DD HH:mm:ss.SSS')} readOnly /> <Input
value={moment(producerNode.traceNode.endTimestamp).format('YYYY-MM-DD HH:mm:ss.SSS')}
readOnly/>
</Form.Item> </Form.Item>
<Form.Item label={<Text strong>{t.COST_TIME}</Text>}> <Form.Item label={<Text strong>{t.COST_TIME}</Text>}>
<Input value={`${producerNode.traceNode.costTime === 0 ? '<1' : producerNode.traceNode.costTime}ms`} readOnly /> <Input
value={`${producerNode.traceNode.costTime === 0 ? '<1' : producerNode.traceNode.costTime}ms`}
readOnly/>
</Form.Item> </Form.Item>
<Form.Item label={<Text strong>{t.MSG_TYPE}</Text>}> <Form.Item label={<Text strong>{t.MSG_TYPE}</Text>}>
<Input value={producerNode.traceNode.msgType} readOnly /> <Input value={producerNode.traceNode.msgType} readOnly/>
</Form.Item> </Form.Item>
<Form.Item label={<Text strong>{t.CLIENT_HOST}</Text>}> <Form.Item label={<Text strong>{t.CLIENT_HOST}</Text>}>
<Input value={producerNode.traceNode.clientHost} readOnly /> <Input value={producerNode.traceNode.clientHost} readOnly/>
</Form.Item> </Form.Item>
<Form.Item label={<Text strong>{t.STORE_HOST}</Text>}> <Form.Item label={<Text strong>{t.STORE_HOST}</Text>}>
<Input value={producerNode.traceNode.storeHost} readOnly /> <Input value={producerNode.traceNode.storeHost} readOnly/>
</Form.Item> </Form.Item>
<Form.Item label={<Text strong>{t.RETRY_TIMES}</Text>}> <Form.Item label={<Text strong>{t.RETRY_TIMES}</Text>}>
<Input value={producerNode.traceNode.retryTimes} readOnly /> <Input value={producerNode.traceNode.retryTimes} readOnly/>
</Form.Item> </Form.Item>
<Form.Item label={<Text strong>{t.OFFSET_MSG_ID}</Text>}> <Form.Item label={<Text strong>{t.OFFSET_MSG_ID}</Text>}>
<Input value={producerNode.offSetMsgId} readOnly /> <Input value={producerNode.offSetMsgId} readOnly/>
</Form.Item> </Form.Item>
</div> </div>
</Form> </Form>
{producerNode.transactionNodeList && producerNode.transactionNodeList.length > 0 && ( {producerNode.transactionNodeList && producerNode.transactionNodeList.length > 0 && (
<div style={{ marginTop: '30px' }}> <div style={{marginTop: '30px'}}>
<Typography.Title level={4} style={{ marginBottom: '15px' }}>{t.CHECK_TRANSACTION_INFO}:</Typography.Title> <Typography.Title level={4}
style={{marginBottom: '15px'}}>{t.CHECK_TRANSACTION_INFO}:</Typography.Title>
<Table <Table
columns={transactionColumns} columns={transactionColumns}
dataSource={producerNode.transactionNodeList} dataSource={producerNode.transactionNodeList}
@@ -418,7 +506,7 @@ const MessageTraceDetailViewDialog = ({ ngDialogData }) => {
bordered bordered
pagination={false} pagination={false}
size="middle" size="middle"
scroll={{ x: 'max-content' }} scroll={{x: 'max-content'}}
/> />
</div> </div>
)} )}
@@ -428,22 +516,31 @@ const MessageTraceDetailViewDialog = ({ ngDialogData }) => {
</Collapse> </Collapse>
</div> </div>
<div style={{ borderRadius: '8px', overflow: 'hidden', boxShadow: '0 4px 12px rgba(0, 0, 0, 0.08)' }}> <div style={{borderRadius: '8px', overflow: 'hidden', boxShadow: '0 4px 12px rgba(0, 0, 0, 0.08)'}}>
<Collapse defaultActiveKey={['consumeMessageTrace']} expandIconPosition="right"> <Collapse defaultActiveKey={['consumeMessageTrace']} expandIconPosition="right">
<Panel header={<Typography.Title level={3} style={{ margin: 0, color: '#333' }}>{t.CONSUME_MESSAGE_TRACE}</Typography.Title>} key="consumeMessageTrace"> <Panel header={<Typography.Title level={3} style={{
margin: 0,
color: '#333'
}}>{t.CONSUME_MESSAGE_TRACE}</Typography.Title>} key="consumeMessageTrace">
{subscriptionNodeList.length === 0 ? ( {subscriptionNodeList.length === 0 ? (
<Paragraph style={{ padding: '16px', textAlign: 'center', color: '#666' }}>{t.NO_CONSUMER_TRACE_DATA}</Paragraph> <Paragraph style={{
padding: '16px',
textAlign: 'center',
color: '#666'
}}>{t.NO_CONSUMER_TRACE_DATA}</Paragraph>
) : ( ) : (
<div style={{ padding: '16px', backgroundColor: '#fff' }}> <div style={{padding: '16px', backgroundColor: '#fff'}}>
{subscriptionNodeList.map(subscriptionNode => ( {subscriptionNodeList.map(subscriptionNode => (
<Collapse <Collapse
key={subscriptionNode.subscriptionGroup} key={subscriptionNode.subscriptionGroup}
style={{ marginBottom: '10px', border: '1px solid #e0e0e0', borderRadius: '4px' }} style={{marginBottom: '10px', border: '1px solid #e0e0e0', borderRadius: '4px'}}
defaultActiveKey={[subscriptionNode.subscriptionGroup]} defaultActiveKey={[subscriptionNode.subscriptionGroup]}
ghost ghost
> >
<Panel <Panel
header={<Typography.Title level={4} style={{ margin: 0 }}>{t.SUBSCRIPTION_GROUP}: <Text strong>{subscriptionNode.subscriptionGroup}</Text></Typography.Title>} header={<Typography.Title level={4}
style={{margin: 0}}>{t.SUBSCRIPTION_GROUP}: <Text
strong>{subscriptionNode.subscriptionGroup}</Text></Typography.Title>}
key={subscriptionNode.subscriptionGroup} key={subscriptionNode.subscriptionGroup}
> >
<Table <Table
@@ -453,7 +550,7 @@ const MessageTraceDetailViewDialog = ({ ngDialogData }) => {
bordered bordered
pagination={false} pagination={false}
size="middle" size="middle"
scroll={{ x: 'max-content' }} scroll={{x: 'max-content'}}
/> />
</Panel> </Panel>
</Collapse> </Collapse>

View File

@@ -16,29 +16,29 @@
*/ */
import React, {useEffect, useState} from 'react'; import React, {useEffect, useState} from 'react';
import { Layout, Menu, Dropdown, Button, Drawer, Grid, Space } from 'antd'; import {Button, Drawer, Dropdown, Grid, Layout, Menu, Space} from 'antd';
import {GlobalOutlined, DownOutlined, UserOutlined, MenuOutlined, BgColorsOutlined} from '@ant-design/icons'; import {BgColorsOutlined, DownOutlined, GlobalOutlined, MenuOutlined, UserOutlined} from '@ant-design/icons';
import { useLocation, useNavigate } from 'react-router-dom'; import {useLocation, useNavigate} from 'react-router-dom';
import { useLanguage } from '../i18n/LanguageContext'; import {useLanguage} from '../i18n/LanguageContext';
import {useTheme} from "../store/context/ThemeContext"; import {useTheme} from "../store/context/ThemeContext";
import {remoteApi} from "../api/remoteApi/remoteApi"; import {remoteApi} from "../api/remoteApi/remoteApi";
const { Header } = Layout; const {Header} = Layout;
const { useBreakpoint } = Grid; // Used to determine screen breakpoints const {useBreakpoint} = Grid; // Used to determine screen breakpoints
const Navbar = ({ rmqVersion = true, showAcl = true}) => { const Navbar = ({rmqVersion = true, showAcl = true}) => {
const location = useLocation(); const location = useLocation();
const navigate = useNavigate(); const navigate = useNavigate();
const { lang, setLang, t } = useLanguage(); const {lang, setLang, t} = useLanguage();
const screens = useBreakpoint(); // Get current screen size breakpoints const screens = useBreakpoint(); // Get current screen size breakpoints
const { currentThemeName, setCurrentThemeName } = useTheme(); const {currentThemeName, setCurrentThemeName} = useTheme();
const [userName, setUserName] = useState(null); const [userName, setUserName] = useState(null);
const [drawerVisible, setDrawerVisible] = useState(false); // Controls drawer visibility const [drawerVisible, setDrawerVisible] = useState(false); // Controls drawer visibility
// Get selected menu item key based on current route path // Get selected menu item key based on current route path
const getPath = () => location.pathname.replace('/', ''); const getPath = () => location.pathname.replace('/', '');
const handleMenuClick = ({ key }) => { const handleMenuClick = ({key}) => {
navigate(`/${key}`); navigate(`/${key}`);
setDrawerVisible(false); // Close drawer after clicking a menu item setDrawerVisible(false); // Close drawer after clicking a menu item
}; };
@@ -63,13 +63,13 @@ const Navbar = ({ rmqVersion = true, showAcl = true}) => {
const storedUsername = window.localStorage.getItem("username"); const storedUsername = window.localStorage.getItem("username");
if (storedUsername) { if (storedUsername) {
setUserName(storedUsername); setUserName(storedUsername);
}else { } else {
setUserName(null); setUserName(null);
} }
}, []); }, []);
const langMenu = ( const langMenu = (
<Menu onClick={({ key }) => setLang(key)}> <Menu onClick={({key}) => setLang(key)}>
<Menu.Item key="en">{t.ENGLISH}</Menu.Item> <Menu.Item key="en">{t.ENGLISH}</Menu.Item>
<Menu.Item key="zh">{t.CHINESE}</Menu.Item> <Menu.Item key="zh">{t.CHINESE}</Menu.Item>
</Menu> </Menu>
@@ -82,7 +82,7 @@ const Navbar = ({ rmqVersion = true, showAcl = true}) => {
); );
const themeMenu = ( const themeMenu = (
<Menu onClick={({ key }) => setCurrentThemeName(key)}> <Menu onClick={({key}) => setCurrentThemeName(key)}>
<Menu.Item key="default">{t.BLUE} ({t.DEFAULT})</Menu.Item> <Menu.Item key="default">{t.BLUE} ({t.DEFAULT})</Menu.Item>
<Menu.Item key="pink">{t.PINK}</Menu.Item> <Menu.Item key="pink">{t.PINK}</Menu.Item>
<Menu.Item key="green">{t.GREEN}</Menu.Item> <Menu.Item key="green">{t.GREEN}</Menu.Item>
@@ -92,17 +92,17 @@ const Navbar = ({ rmqVersion = true, showAcl = true}) => {
// Menu item configuration // Menu item configuration
const menuItems = [ const menuItems = [
{ key: 'ops', label: t.OPS }, {key: 'ops', label: t.OPS},
...(rmqVersion ? [{ key: 'proxy', label: t.PROXY }] : []), ...(rmqVersion ? [{key: 'proxy', label: t.PROXY}] : []),
{ key: '', label: t.DASHBOARD }, // Dashboard corresponds to root path {key: '', label: t.DASHBOARD}, // Dashboard corresponds to root path
{ key: 'cluster', label: t.CLUSTER }, {key: 'cluster', label: t.CLUSTER},
{ key: 'topic', label: t.TOPIC }, {key: 'topic', label: t.TOPIC},
{ key: 'consumer', label: t.CONSUMER }, {key: 'consumer', label: t.CONSUMER},
{ key: 'producer', label: t.PRODUCER }, {key: 'producer', label: t.PRODUCER},
{ key: 'message', label: t.MESSAGE }, {key: 'message', label: t.MESSAGE},
{ key: 'dlqMessage', label: t.DLQ_MESSAGE }, {key: 'dlqMessage', label: t.DLQ_MESSAGE},
{ key: 'messageTrace', label: t.MESSAGETRACE }, {key: 'messageTrace', label: t.MESSAGETRACE},
...(showAcl ? [{ key: 'acl', label: t.WHITE_LIST }] : []), ...(showAcl ? [{key: 'acl', label: t.ACL_MANAGEMENT}] : []),
]; ];
// Determine if it's a small screen (e.g., less than md) // Determine if it's a small screen (e.g., less than md)
@@ -120,7 +120,7 @@ const Navbar = ({ rmqVersion = true, showAcl = true}) => {
padding: isExtraSmallScreen ? '0 16px' : '0 24px', // Smaller padding on extra small screens padding: isExtraSmallScreen ? '0 16px' : '0 24px', // Smaller padding on extra small screens
}} }}
> >
<div className="navbar-left" style={{ display: 'flex', alignItems: 'center' }}> <div className="navbar-left" style={{display: 'flex', alignItems: 'center'}}>
<div <div
style={{ style={{
fontWeight: 'bold', fontWeight: 'bold',
@@ -141,33 +141,33 @@ const Navbar = ({ rmqVersion = true, showAcl = true}) => {
mode="horizontal" mode="horizontal"
items={menuItems} items={menuItems}
theme="dark" // Use dark theme to match Header background theme="dark" // Use dark theme to match Header background
style={{ flex: 1, minWidth: 0 }} // Allow menu items to adapt width style={{flex: 1, minWidth: 0}} // Allow menu items to adapt width
/> />
)} )}
</div> </div>
<Space size={isExtraSmallScreen ? 8 : 16} > {/* Adjust spacing for buttons */} <Space size={isExtraSmallScreen ? 8 : 16}> {/* Adjust spacing for buttons */}
{/* Theme switch button */} {/* Theme switch button */}
<Dropdown overlay={themeMenu}> <Dropdown overlay={themeMenu}>
<Button icon={<BgColorsOutlined />} size="small"> <Button icon={<BgColorsOutlined/>} size="small">
{!isExtraSmallScreen && `${t.TOPIC}: ${currentThemeName}`} {!isExtraSmallScreen && `${t.TOPIC}: ${currentThemeName}`}
<DownOutlined /> <DownOutlined/>
</Button> </Button>
</Dropdown> </Dropdown>
<Dropdown overlay={langMenu}> <Dropdown overlay={langMenu}>
<Button icon={<GlobalOutlined />} size="small"> <Button icon={<GlobalOutlined/>} size="small">
{!isExtraSmallScreen && t.CHANGE_LANG} {/* Hide text on extra small screens */} {!isExtraSmallScreen && t.CHANGE_LANG} {/* Hide text on extra small screens */}
<DownOutlined /> <DownOutlined/>
</Button> </Button>
</Dropdown> </Dropdown>
{userName && ( {userName && (
<Dropdown overlay={userMenu}> <Dropdown overlay={userMenu}>
{/* 使用一个可点击的元素作为 Dropdown 的唯一子元素 */} {/* 使用一个可点击的元素作为 Dropdown 的唯一子元素 */}
<a onClick={e => e.preventDefault()} style={{ display: 'flex', alignItems: 'center' }}> <a onClick={e => e.preventDefault()} style={{display: 'flex', alignItems: 'center'}}>
<UserOutlined style={{ marginRight: 8 }} /> {/* 添加一些间距 */} <UserOutlined style={{marginRight: 8}}/> {/* 添加一些间距 */}
{userName} {userName}
<DownOutlined style={{ marginLeft: 8 }} /> <DownOutlined style={{marginLeft: 8}}/>
</a> </a>
</Dropdown> </Dropdown>
)} )}
@@ -175,9 +175,9 @@ const Navbar = ({ rmqVersion = true, showAcl = true}) => {
{isSmallScreen && ( // Display hamburger icon on small screens {isSmallScreen && ( // Display hamburger icon on small screens
<Button <Button
type="primary" type="primary"
icon={<MenuOutlined />} icon={<MenuOutlined/>}
onClick={() => setDrawerVisible(true)} onClick={() => setDrawerVisible(true)}
style={{ marginLeft: isExtraSmallScreen ? 8 : 16 }} // Adjust margin for hamburger icon style={{marginLeft: isExtraSmallScreen ? 8 : 16}} // Adjust margin for hamburger icon
/> />
)} )}
</Space> </Space>
@@ -192,7 +192,7 @@ const Navbar = ({ rmqVersion = true, showAcl = true}) => {
open={drawerVisible} open={drawerVisible}
// If you want the Drawer's background to match the Menu's background color, you can set bodyStyle like this // If you want the Drawer's background to match the Menu's background color, you can set bodyStyle like this
// or set components.Drawer.colorBgElevated in theme.js, etc. // or set components.Drawer.colorBgElevated in theme.js, etc.
bodyStyle={{ padding: 0, backgroundColor: '#1c324a' }} // Set Drawer body background to dark bodyStyle={{padding: 0, backgroundColor: '#1c324a'}} // Set Drawer body background to dark
width={200} // Set drawer width width={200} // Set drawer width
> >
<Menu <Menu
@@ -201,7 +201,7 @@ const Navbar = ({ rmqVersion = true, showAcl = true}) => {
mode="inline" // Use vertical menu in drawer mode="inline" // Use vertical menu in drawer
items={menuItems} items={menuItems}
theme="dark" theme="dark"
style={{ height: '100%', borderRight: 0 }} // Ensure menu fills the drawer style={{height: '100%', borderRight: 0}} // Ensure menu fills the drawer
/> />
</Drawer> </Drawer>
</Header> </Header>

View File

@@ -15,23 +15,23 @@
* limitations under the License. * limitations under the License.
*/ */
import { Input, Select, Tag, Space } from 'antd'; import {Input, Select, Space, Tag} from 'antd';
import { PlusOutlined } from '@ant-design/icons'; import {PlusOutlined} from '@ant-design/icons';
import React, { useState } from 'react'; import React, {useState} from 'react';
const { Option } = Select; const {Option} = Select;
// 资源类型枚举 // 资源类型枚举
const resourceTypes = [ const resourceTypes = [
{ value: 0, label: 'Unknown', prefix: 'UNKNOWN' }, {value: 0, label: 'Unknown', prefix: 'UNKNOWN'},
{ value: 1, label: 'Any', prefix: 'ANY' }, {value: 1, label: 'Any', prefix: 'ANY'},
{ value: 2, label: 'Cluster', prefix: 'CLUSTER' }, {value: 2, label: 'Cluster', prefix: 'CLUSTER'},
{ value: 3, label: 'Namespace', prefix: 'NAMESPACE' }, {value: 3, label: 'Namespace', prefix: 'NAMESPACE'},
{ value: 4, label: 'Topic', prefix: 'TOPIC' }, {value: 4, label: 'Topic', prefix: 'TOPIC'},
{ value: 5, label: 'Group', prefix: 'GROUP' }, {value: 5, label: 'Group', prefix: 'GROUP'},
]; ];
const ResourceInput = ({ value = [], onChange }) => { const ResourceInput = ({value = [], onChange}) => {
// 确保 value 始终是数组 // 确保 value 始终是数组
const safeValue = Array.isArray(value) ? value : []; const safeValue = Array.isArray(value) ? value : [];
@@ -96,7 +96,7 @@ const ResourceInput = ({ value = [], onChange }) => {
<Space> <Space>
<Select <Select
value={selectedType} value={selectedType}
style={{ width: 120 }} style={{width: 120}}
onChange={handleTypeChange} onChange={handleTypeChange}
> >
{resourceTypes.map(type => ( {resourceTypes.map(type => (
@@ -107,7 +107,7 @@ const ResourceInput = ({ value = [], onChange }) => {
</Select> </Select>
<Input <Input
ref={inputRef} ref={inputRef}
style={{ width: 180 }} style={{width: 180}}
value={resourceName} value={resourceName}
onChange={handleNameChange} onChange={handleNameChange}
onPressEnter={handleAddResource} onPressEnter={handleAddResource}
@@ -116,8 +116,8 @@ const ResourceInput = ({ value = [], onChange }) => {
/> />
</Space> </Space>
) : ( ) : (
<Tag onClick={showInput} style={{ background: '#fff', borderStyle: 'dashed' }}> <Tag onClick={showInput} style={{background: '#fff', borderStyle: 'dashed'}}>
<PlusOutlined /> 添加资源 <PlusOutlined/> 添加资源
</Tag> </Tag>
)} )}
</Space> </Space>

View File

@@ -15,27 +15,27 @@
* limitations under the License. * limitations under the License.
*/ */
import { Input, Select } from 'antd'; import {Input, Select} from 'antd';
import React, { useState, useEffect } from 'react'; import React, {useEffect, useState} from 'react';
const { Option } = Select; const {Option} = Select;
// Subject 类型枚举 // Subject 类型枚举
const subjectTypes = [ const subjectTypes = [
{ value: 'User', label: 'User' }, {value: 'User', label: 'User'},
]; ];
const SubjectInput = ({ value, onChange, disabled }) => { const SubjectInput = ({value, onChange, disabled}) => {
// 解析传入的 value将其拆分为 type 和 name // 解析传入的 value将其拆分为 type 和 name
const parseValue = (val) => { const parseValue = (val) => {
if (!val || typeof val !== 'string') { if (!val || typeof val !== 'string') {
return { type: subjectTypes[0].value, name: '' }; // 默认值 return {type: subjectTypes[0].value, name: ''}; // 默认值
} }
const parts = val.split(':'); const parts = val.split(':');
if (parts.length === 2 && subjectTypes.some(t => t.value === parts[0])) { if (parts.length === 2 && subjectTypes.some(t => t.value === parts[0])) {
return { type: parts[0], name: parts[1] }; return {type: parts[0], name: parts[1]};
} }
return { type: subjectTypes[0].value, name: val }; // 如果格式不匹配,将整个值作为 name类型设为默认 return {type: subjectTypes[0].value, name: val}; // 如果格式不匹配,将整个值作为 name类型设为默认
}; };
const [currentType, setCurrentType] = useState(() => parseValue(value).type); const [currentType, setCurrentType] = useState(() => parseValue(value).type);
@@ -76,7 +76,7 @@ const SubjectInput = ({ value, onChange, disabled }) => {
return ( return (
<Input.Group compact> <Input.Group compact>
<Select <Select
style={{ width: '30%' }} style={{width: '30%'}}
value={currentType} value={currentType}
onChange={onTypeChange} onChange={onTypeChange}
disabled={disabled} disabled={disabled}
@@ -88,7 +88,7 @@ const SubjectInput = ({ value, onChange, disabled }) => {
))} ))}
</Select> </Select>
<Input <Input
style={{ width: '70%' }} style={{width: '70%'}}
value={currentName} value={currentName}
onChange={onNameChange} onChange={onNameChange}
placeholder="请输入名称 (例如: yourUsername)" placeholder="请输入名称 (例如: yourUsername)"

View File

@@ -15,13 +15,13 @@
* limitations under the License. * limitations under the License.
*/ */
import React, { useState, useEffect } from 'react'; import React, {useEffect, useState} from 'react';
import { Modal, Table, Spin } from 'antd'; import {Modal, Spin, Table} from 'antd';
import { remoteApi } from '../../api/remoteApi/remoteApi'; import {remoteApi} from '../../api/remoteApi/remoteApi';
import { useLanguage } from '../../i18n/LanguageContext'; import {useLanguage} from '../../i18n/LanguageContext';
const ClientInfoModal = ({ visible, group, address, onCancel }) => { const ClientInfoModal = ({visible, group, address, onCancel}) => {
const { t } = useLanguage(); const {t} = useLanguage();
const [loading, setLoading] = useState(false); const [loading, setLoading] = useState(false);
const [connectionData, setConnectionData] = useState(null); const [connectionData, setConnectionData] = useState(null);
const [subscriptionData, setSubscriptionData] = useState(null); const [subscriptionData, setSubscriptionData] = useState(null);
@@ -46,15 +46,15 @@ const ClientInfoModal = ({ visible, group, address, onCancel }) => {
}, [visible, group, address]); }, [visible, group, address]);
const connectionColumns = [ const connectionColumns = [
{ title: 'ClientId', dataIndex: 'clientId' }, {title: 'ClientId', dataIndex: 'clientId'},
{ title: 'ClientAddr', dataIndex: 'clientAddr' }, {title: 'ClientAddr', dataIndex: 'clientAddr'},
{ title: 'Language', dataIndex: 'language' }, {title: 'Language', dataIndex: 'language'},
{ title: 'Version', dataIndex: 'versionDesc' }, {title: 'Version', dataIndex: 'versionDesc'},
]; ];
const subscriptionColumns = [ const subscriptionColumns = [
{ title: 'Topic', dataIndex: 'topic' }, {title: 'Topic', dataIndex: 'topic'},
{ title: 'SubExpression', dataIndex: 'subString' }, {title: 'SubExpression', dataIndex: 'subString'},
]; ];
return ( return (
@@ -88,7 +88,7 @@ const ClientInfoModal = ({ visible, group, address, onCancel }) => {
rowKey="topic" rowKey="topic"
pagination={false} pagination={false}
locale={{ locale={{
emptyText: loading ? <Spin size="small" /> : t.NO_DATA emptyText: loading ? <Spin size="small"/> : t.NO_DATA
}} }}
/> />
<p>ConsumeType: {connectionData.consumeType}</p> <p>ConsumeType: {connectionData.consumeType}</p>

View File

@@ -15,13 +15,23 @@
* limitations under the License. * limitations under the License.
*/ */
import React, { useEffect, useState } from 'react'; import React, {useEffect, useState} from 'react';
import { Button, Descriptions, Form, Input, Select, Switch, message } from 'antd'; import {Button, Descriptions, Form, Input, message, Select, Switch} from 'antd';
import { remoteApi } from '../../api/remoteApi/remoteApi'; // 确保路径正确 import {remoteApi} from '../../api/remoteApi/remoteApi'; // 确保路径正确
const { Option } = Select; const {Option} = Select;
const ConsumerConfigItem = ({ initialConfig, isAddConfig, group, brokerName, allBrokerList, allClusterNames,onCancel, onSuccess, t }) => { const ConsumerConfigItem = ({
initialConfig,
isAddConfig,
group,
brokerName,
allBrokerList,
allClusterNames,
onCancel,
onSuccess,
t
}) => {
const [form] = Form.useForm(); const [form] = Form.useForm();
const [currentBrokerName, setCurrentBrokerName] = useState(brokerName); const [currentBrokerName, setCurrentBrokerName] = useState(brokerName);

View File

@@ -34,7 +34,7 @@ const ConsumerConfigModal = ({visible, isAddConfig, group, onCancel, setIsAddCon
setLoading(true); setLoading(true);
try { try {
// Fetch cluster list for broker names and cluster names // Fetch cluster list for broker names and cluster names
if(isAddConfig) { if (isAddConfig) {
const clusterResponse = await remoteApi.getClusterList(); const clusterResponse = await remoteApi.getClusterList();
if (clusterResponse.status === 0 && clusterResponse.data) { if (clusterResponse.status === 0 && clusterResponse.data) {
const clusterInfo = clusterResponse.data.clusterInfo; const clusterInfo = clusterResponse.data.clusterInfo;

View File

@@ -15,13 +15,13 @@
* limitations under the License. * limitations under the License.
*/ */
import React, { useState, useEffect } from 'react'; import React, {useEffect, useState} from 'react';
import { Modal, Table, Spin } from 'antd'; import {Modal, Spin, Table} from 'antd';
import { remoteApi } from '../../api/remoteApi/remoteApi'; import {remoteApi} from '../../api/remoteApi/remoteApi';
import { useLanguage } from '../../i18n/LanguageContext'; import {useLanguage} from '../../i18n/LanguageContext';
const ConsumerDetailModal = ({ visible, group, address, onCancel }) => { const ConsumerDetailModal = ({visible, group, address, onCancel}) => {
const { t } = useLanguage(); const {t} = useLanguage();
const [loading, setLoading] = useState(false); const [loading, setLoading] = useState(false);
const [details, setDetails] = useState([]); const [details, setDetails] = useState([]);
@@ -44,12 +44,12 @@ const ConsumerDetailModal = ({ visible, group, address, onCancel }) => {
}, [visible, group, address]); }, [visible, group, address]);
const queueColumns = [ const queueColumns = [
{ title: 'Broker', dataIndex: 'brokerName' }, {title: 'Broker', dataIndex: 'brokerName'},
{ title: 'Queue', dataIndex: 'queueId' }, {title: 'Queue', dataIndex: 'queueId'},
{ title: 'BrokerOffset', dataIndex: 'brokerOffset' }, {title: 'BrokerOffset', dataIndex: 'brokerOffset'},
{ title: 'ConsumerOffset', dataIndex: 'consumerOffset' }, {title: 'ConsumerOffset', dataIndex: 'consumerOffset'},
{ title: 'DiffTotal', dataIndex: 'diffTotal' }, {title: 'DiffTotal', dataIndex: 'diffTotal'},
{ title: 'LastTimestamp', dataIndex: 'lastTimestamp' }, {title: 'LastTimestamp', dataIndex: 'lastTimestamp'},
]; ];
return ( return (

View File

@@ -15,13 +15,13 @@
* limitations under the License. * limitations under the License.
*/ */
import React, { useState, useEffect } from 'react'; import React, {useEffect, useState} from 'react';
import { Modal, Spin, Checkbox, Button, notification } from 'antd'; import {Button, Checkbox, Modal, notification, Spin} from 'antd';
import { remoteApi } from '../../api/remoteApi/remoteApi'; import {remoteApi} from '../../api/remoteApi/remoteApi';
import { useLanguage } from '../../i18n/LanguageContext'; import {useLanguage} from '../../i18n/LanguageContext';
const DeleteConsumerModal = ({ visible, group, onCancel, onSuccess }) => { const DeleteConsumerModal = ({visible, group, onCancel, onSuccess}) => {
const { t } = useLanguage(); const {t} = useLanguage();
const [brokerList, setBrokerList] = useState([]); const [brokerList, setBrokerList] = useState([]);
const [selectedBrokers, setSelectedBrokers] = useState([]); const [selectedBrokers, setSelectedBrokers] = useState([]);
const [loading, setLoading] = useState(false); const [loading, setLoading] = useState(false);
@@ -48,7 +48,7 @@ const DeleteConsumerModal = ({ visible, group, onCancel, onSuccess }) => {
// 处理删除提交 // 处理删除提交
const handleDelete = async () => { const handleDelete = async () => {
if (selectedBrokers.length === 0) { if (selectedBrokers.length === 0) {
notification.warning({ message: t.PLEASE_SELECT_BROKER }); notification.warning({message: t.PLEASE_SELECT_BROKER});
return; return;
} }
@@ -60,7 +60,7 @@ const DeleteConsumerModal = ({ visible, group, onCancel, onSuccess }) => {
); );
if (response.status === 0) { if (response.status === 0) {
notification.success({ message: t.DELETE_SUCCESS }); notification.success({message: t.DELETE_SUCCESS});
onSuccess(); onSuccess();
onCancel(); onCancel();
} }
@@ -90,9 +90,9 @@ const DeleteConsumerModal = ({ visible, group, onCancel, onSuccess }) => {
]} ]}
> >
<Spin spinning={loading}> <Spin spinning={loading}>
<div style={{ marginBottom: 16 }}>{t.SELECT_DELETE_BROKERS}:</div> <div style={{marginBottom: 16}}>{t.SELECT_DELETE_BROKERS}:</div>
<Checkbox.Group <Checkbox.Group
style={{ width: '100%' }} style={{width: '100%'}}
value={selectedBrokers} value={selectedBrokers}
onChange={values => setSelectedBrokers(values)} onChange={values => setSelectedBrokers(values)}
> >

View File

@@ -15,10 +15,10 @@
* limitations under the License. * limitations under the License.
*/ */
import { Button, DatePicker, Form, Modal, Select } from "antd"; import {Button, DatePicker, Form, Modal, Select} from "antd";
import React, { useEffect, useState } from "react"; import React, {useEffect, useState} from "react";
const ConsumerResetOffsetDialog = ({ visible, onClose, topic, allConsumerGroupList, handleResetOffset, t }) => { const ConsumerResetOffsetDialog = ({visible, onClose, topic, allConsumerGroupList, handleResetOffset, t}) => {
const [form] = Form.useForm(); const [form] = Form.useForm();
const [selectedConsumerGroup, setSelectedConsumerGroup] = useState([]); const [selectedConsumerGroup, setSelectedConsumerGroup] = useState([]);
const [selectedTime, setSelectedTime] = useState(null); const [selectedTime, setSelectedTime] = useState(null);
@@ -49,14 +49,14 @@ const ConsumerResetOffsetDialog = ({ visible, onClose, topic, allConsumerGroupLi
</Button>, </Button>,
]} ]}
> >
<Form form={form} layout="horizontal" labelCol={{ span: 6 }} wrapperCol={{ span: 18 }}> <Form form={form} layout="horizontal" labelCol={{span: 6}} wrapperCol={{span: 18}}>
<Form.Item label={t.SUBSCRIPTION_GROUP} required> <Form.Item label={t.SUBSCRIPTION_GROUP} required>
<Select <Select
mode="multiple" mode="multiple"
placeholder={t.SELECT_CONSUMER_GROUP} placeholder={t.SELECT_CONSUMER_GROUP}
value={selectedConsumerGroup} value={selectedConsumerGroup}
onChange={setSelectedConsumerGroup} onChange={setSelectedConsumerGroup}
options={allConsumerGroupList.map(group => ({ value: group, label: group }))} options={allConsumerGroupList.map(group => ({value: group, label: group}))}
/> />
</Form.Item> </Form.Item>
<Form.Item label={t.TIME} required> <Form.Item label={t.TIME} required>
@@ -65,7 +65,7 @@ const ConsumerResetOffsetDialog = ({ visible, onClose, topic, allConsumerGroupLi
format="YYYY-MM-DD HH:mm:ss" format="YYYY-MM-DD HH:mm:ss"
value={selectedTime} value={selectedTime}
onChange={setSelectedTime} onChange={setSelectedTime}
style={{ width: '100%' }} style={{width: '100%'}}
/> />
</Form.Item> </Form.Item>
</Form> </Form>

View File

@@ -19,13 +19,13 @@ import moment from "moment/moment";
import {Button, Modal, Table} from "antd"; import {Button, Modal, Table} from "antd";
import React from "react"; import React from "react";
const ConsumerViewDialog = ({ visible, onClose, topic, consumerData, consumerGroupCount, t }) => { const ConsumerViewDialog = ({visible, onClose, topic, consumerData, consumerGroupCount, t}) => {
const columns = [ const columns = [
{ title: t.BROKER, dataIndex: 'brokerName', key: 'brokerName', align: 'center' }, {title: t.BROKER, dataIndex: 'brokerName', key: 'brokerName', align: 'center'},
{ title: t.QUEUE, dataIndex: 'queueId', key: 'queueId', align: 'center' }, {title: t.QUEUE, dataIndex: 'queueId', key: 'queueId', align: 'center'},
{ title: t.CONSUMER_CLIENT, dataIndex: 'clientInfo', key: 'clientInfo', align: 'center' }, {title: t.CONSUMER_CLIENT, dataIndex: 'clientInfo', key: 'clientInfo', align: 'center'},
{ title: t.BROKER_OFFSET, dataIndex: 'brokerOffset', key: 'brokerOffset', align: 'center' }, {title: t.BROKER_OFFSET, dataIndex: 'brokerOffset', key: 'brokerOffset', align: 'center'},
{ title: t.CONSUMER_OFFSET, dataIndex: 'consumerOffset', key: 'consumerOffset', align: 'center' }, {title: t.CONSUMER_OFFSET, dataIndex: 'consumerOffset', key: 'consumerOffset', align: 'center'},
{ {
title: t.DIFF_TOTAL, title: t.DIFF_TOTAL,
dataIndex: 'diffTotal', dataIndex: 'diffTotal',
@@ -58,15 +58,19 @@ const ConsumerViewDialog = ({ visible, onClose, topic, consumerData, consumerGro
<div>{t.NO_DATA} {t.SUBSCRIPTION_GROUP}</div> <div>{t.NO_DATA} {t.SUBSCRIPTION_GROUP}</div>
) : ( ) : (
consumerData && Object.entries(consumerData).map(([consumerGroup, consumeDetail]) => ( consumerData && Object.entries(consumerData).map(([consumerGroup, consumeDetail]) => (
<div key={consumerGroup} style={{ marginBottom: '24px' }}> <div key={consumerGroup} style={{marginBottom: '24px'}}>
<Table <Table
bordered bordered
pagination={false} pagination={false}
showHeader={false} showHeader={false}
dataSource={[{ consumerGroup, diffTotal: consumeDetail.diffTotal, lastTimestamp: consumeDetail.lastTimestamp }]} dataSource={[{
consumerGroup,
diffTotal: consumeDetail.diffTotal,
lastTimestamp: consumeDetail.lastTimestamp
}]}
columns={[ columns={[
{ title: t.SUBSCRIPTION_GROUP, dataIndex: 'consumerGroup', key: 'consumerGroup' }, {title: t.SUBSCRIPTION_GROUP, dataIndex: 'consumerGroup', key: 'consumerGroup'},
{ title: t.DELAY, dataIndex: 'diffTotal', key: 'diffTotal' }, {title: t.DELAY, dataIndex: 'diffTotal', key: 'diffTotal'},
{ {
title: t.LAST_CONSUME_TIME, title: t.LAST_CONSUME_TIME,
dataIndex: 'lastTimestamp', dataIndex: 'lastTimestamp',
@@ -76,7 +80,7 @@ const ConsumerViewDialog = ({ visible, onClose, topic, consumerData, consumerGro
]} ]}
rowKey="consumerGroup" rowKey="consumerGroup"
size="small" size="small"
style={{ marginBottom: '12px' }} style={{marginBottom: '12px'}}
/> />
<Table <Table
bordered bordered

View File

@@ -18,7 +18,7 @@
import {Button, Modal, Table} from "antd"; import {Button, Modal, Table} from "antd";
import React from "react"; import React from "react";
const ResetOffsetResultDialog = ({ visible, onClose, result, t }) => { const ResetOffsetResultDialog = ({visible, onClose, result, t}) => {
return ( return (
<Modal <Modal
title="ResetResult" title="ResetResult"
@@ -31,12 +31,12 @@ const ResetOffsetResultDialog = ({ visible, onClose, result, t }) => {
]} ]}
> >
{result && Object.entries(result).map(([groupName, groupData]) => ( {result && Object.entries(result).map(([groupName, groupData]) => (
<div key={groupName} style={{ marginBottom: '16px', border: '1px solid #f0f0f0', padding: '10px' }}> <div key={groupName} style={{marginBottom: '16px', border: '1px solid #f0f0f0', padding: '10px'}}>
<Table <Table
dataSource={[{ groupName, status: groupData.status }]} dataSource={[{groupName, status: groupData.status}]}
columns={[ columns={[
{ title: 'GroupName', dataIndex: 'groupName', key: 'groupName' }, {title: 'GroupName', dataIndex: 'groupName', key: 'groupName'},
{ title: 'State', dataIndex: 'status', key: 'status' }, {title: 'State', dataIndex: 'status', key: 'status'},
]} ]}
pagination={false} pagination={false}
rowKey="groupName" rowKey="groupName"
@@ -47,8 +47,8 @@ const ResetOffsetResultDialog = ({ visible, onClose, result, t }) => {
<div>You Should Check It Yourself</div> <div>You Should Check It Yourself</div>
) : ( ) : (
<Table <Table
dataSource={groupData.rollbackStatsList.map((item, index) => ({ key: index, item }))} dataSource={groupData.rollbackStatsList.map((item, index) => ({key: index, item}))}
columns={[{ dataIndex: 'item', key: 'item' }]} columns={[{dataIndex: 'item', key: 'item'}]}
pagination={false} pagination={false}
rowKey="key" rowKey="key"
size="small" size="small"

View File

@@ -15,10 +15,10 @@
* limitations under the License. * limitations under the License.
*/ */
import { Button, Modal, Table } from "antd"; import {Button, Modal, Table} from "antd";
import React from "react"; import React from "react";
const RouterViewDialog = ({ visible, onClose, topic, routeData, t }) => { const RouterViewDialog = ({visible, onClose, topic, routeData, t}) => {
const brokerColumns = [ const brokerColumns = [
{ {
title: 'Broker', title: 'Broker',
@@ -30,10 +30,14 @@ const RouterViewDialog = ({ visible, onClose, topic, routeData, t }) => {
key: 'brokerAddrs', key: 'brokerAddrs',
render: (_, record) => ( render: (_, record) => (
<Table <Table
dataSource={Object.entries(record.brokerAddrs || []).map(([key, value]) => ({ key, idx: key, address: value }))} dataSource={Object.entries(record.brokerAddrs || []).map(([key, value]) => ({
key,
idx: key,
address: value
}))}
columns={[ columns={[
{ title: 'Index', dataIndex: 'idx', key: 'idx' }, {title: 'Index', dataIndex: 'idx', key: 'idx'},
{ title: 'Address', dataIndex: 'address', key: 'address' }, {title: 'Address', dataIndex: 'address', key: 'address'},
]} ]}
pagination={false} pagination={false}
bordered bordered
@@ -82,7 +86,7 @@ const RouterViewDialog = ({ visible, onClose, topic, routeData, t }) => {
<div> <div>
<h3>Broker Datas:</h3> <h3>Broker Datas:</h3>
{routeData?.brokerDatas?.map((item, index) => ( {routeData?.brokerDatas?.map((item, index) => (
<div key={index} style={{ marginBottom: '15px', border: '1px solid #d9d9d9', padding: '10px' }}> <div key={index} style={{marginBottom: '15px', border: '1px solid #d9d9d9', padding: '10px'}}>
<Table <Table
dataSource={[item]} dataSource={[item]}
columns={brokerColumns} columns={brokerColumns}
@@ -93,7 +97,7 @@ const RouterViewDialog = ({ visible, onClose, topic, routeData, t }) => {
</div> </div>
))} ))}
</div> </div>
<div style={{ marginTop: '20px' }}> <div style={{marginTop: '20px'}}>
<h3>{t.QUEUE_DATAS}:</h3> <h3>{t.QUEUE_DATAS}:</h3>
<Table <Table
dataSource={routeData?.queueDatas || []} dataSource={routeData?.queueDatas || []}

View File

@@ -18,7 +18,7 @@
import {Button, Form, Modal, Table} from "antd"; import {Button, Form, Modal, Table} from "antd";
import React from "react"; import React from "react";
const SendResultDialog = ({ visible, onClose, result, t }) => { const SendResultDialog = ({visible, onClose, result, t}) => {
return ( return (
<Modal <Modal
title="SendResult" title="SendResult"
@@ -43,11 +43,11 @@ const SendResultDialog = ({ visible, onClose, result, t }) => {
: [] : []
} }
columns={[ columns={[
{ dataIndex: 'label', key: 'label' }, {dataIndex: 'label', key: 'label'},
{ {
dataIndex: 'value', dataIndex: 'value',
key: 'value', key: 'value',
render: (text) => <pre style={{ whiteSpace: 'pre-wrap', margin: 0 }}>{text}</pre>, render: (text) => <pre style={{whiteSpace: 'pre-wrap', margin: 0}}>{text}</pre>,
}, },
]} ]}
pagination={false} pagination={false}
@@ -61,5 +61,4 @@ const SendResultDialog = ({ visible, onClose, result, t }) => {
}; };
export default SendResultDialog; export default SendResultDialog;

View File

@@ -76,24 +76,24 @@ const SendTopicMessageDialog = ({
</Button>, </Button>,
]} ]}
> >
<Form form={form} layout="horizontal" labelCol={{ span: 6 }} wrapperCol={{ span: 18 }}> <Form form={form} layout="horizontal" labelCol={{span: 6}} wrapperCol={{span: 18}}>
<Form.Item label={t.TOPIC} name="topic"> <Form.Item label={t.TOPIC} name="topic">
<Input disabled /> <Input disabled/>
</Form.Item> </Form.Item>
<Form.Item label={t.TAG} name="tag"> <Form.Item label={t.TAG} name="tag">
<Input /> <Input/>
</Form.Item> </Form.Item>
<Form.Item label={t.KEY} name="key"> <Form.Item label={t.KEY} name="key">
<Input /> <Input/>
</Form.Item> </Form.Item>
<Form.Item label={t.MESSAGE_BODY} name="messageBody" rules={[{ required: true, message: t.REQUIRED }]}> <Form.Item label={t.MESSAGE_BODY} name="messageBody" rules={[{required: true, message: t.REQUIRED}]}>
<Input.TextArea <Input.TextArea
style={{ maxHeight: '200px', minHeight: '200px', resize: 'none' }} style={{maxHeight: '200px', minHeight: '200px', resize: 'none'}}
rows={8} rows={8}
/> />
</Form.Item> </Form.Item>
<Form.Item label={t.ENABLE_MESSAGE_TRACE} name="traceEnabled" valuePropName="checked"> <Form.Item label={t.ENABLE_MESSAGE_TRACE} name="traceEnabled" valuePropName="checked">
<Checkbox /> <Checkbox/>
</Form.Item> </Form.Item>
</Form> </Form>
</Modal> </Modal>

View File

@@ -15,10 +15,17 @@
* limitations under the License. * limitations under the License.
*/ */
import { Button, Form, message, Modal, Select } from "antd"; import {Button, Form, message, Modal, Select} from "antd";
import React, { useEffect, useState } from "react"; import React, {useEffect, useState} from "react";
const SkipMessageAccumulateDialog = ({ visible, onClose, topic, allConsumerGroupList, handleSkipMessageAccumulate, t }) => { const SkipMessageAccumulateDialog = ({
visible,
onClose,
topic,
allConsumerGroupList,
handleSkipMessageAccumulate,
t
}) => {
const [form] = Form.useForm(); const [form] = Form.useForm();
const [selectedConsumerGroup, setSelectedConsumerGroup] = useState([]); const [selectedConsumerGroup, setSelectedConsumerGroup] = useState([]);
@@ -52,14 +59,14 @@ const SkipMessageAccumulateDialog = ({ visible, onClose, topic, allConsumerGroup
</Button>, </Button>,
]} ]}
> >
<Form form={form} layout="horizontal" labelCol={{ span: 6 }} wrapperCol={{ span: 18 }}> <Form form={form} layout="horizontal" labelCol={{span: 6}} wrapperCol={{span: 18}}>
<Form.Item label={t.SUBSCRIPTION_GROUP} required> <Form.Item label={t.SUBSCRIPTION_GROUP} required>
<Select <Select
mode="multiple" mode="multiple"
placeholder={t.SELECT_CONSUMER_GROUP} placeholder={t.SELECT_CONSUMER_GROUP}
value={selectedConsumerGroup} value={selectedConsumerGroup}
onChange={setSelectedConsumerGroup} onChange={setSelectedConsumerGroup}
options={allConsumerGroupList.map(group => ({ value: group, label: group }))} options={allConsumerGroupList.map(group => ({value: group, label: group}))}
/> />
</Form.Item> </Form.Item>
</Form> </Form>

View File

@@ -19,11 +19,11 @@ import moment from "moment/moment";
import {Button, Modal, Table} from "antd"; import {Button, Modal, Table} from "antd";
import React from "react"; import React from "react";
const StatsViewDialog = ({ visible, onClose, topic, statsData, t }) => { const StatsViewDialog = ({visible, onClose, topic, statsData, t}) => {
const columns = [ const columns = [
{ title: t.QUEUE, dataIndex: 'queue', key: 'queue', align: 'center' }, {title: t.QUEUE, dataIndex: 'queue', key: 'queue', align: 'center'},
{ title: t.MIN_OFFSET, dataIndex: 'minOffset', key: 'minOffset', align: 'center' }, {title: t.MIN_OFFSET, dataIndex: 'minOffset', key: 'minOffset', align: 'center'},
{ title: t.MAX_OFFSET, dataIndex: 'maxOffset', key: 'maxOffset', align: 'center' }, {title: t.MAX_OFFSET, dataIndex: 'maxOffset', key: 'maxOffset', align: 'center'},
{ {
title: t.LAST_UPDATE_TIME_STAMP, title: t.LAST_UPDATE_TIME_STAMP,
dataIndex: 'lastUpdateTimestamp', dataIndex: 'lastUpdateTimestamp',

View File

@@ -16,7 +16,7 @@
*/ */
// TopicModifyDialog.js // TopicModifyDialog.js
import { Button, Modal } from "antd"; import {Button, Modal} from "antd";
import React from "react"; import React from "react";
import TopicSingleModifyForm from './TopicSingleModifyForm'; import TopicSingleModifyForm from './TopicSingleModifyForm';
@@ -43,7 +43,7 @@ const TopicModifyDialog = ({
{t.CLOSE} {t.CLOSE}
</Button>, </Button>,
]} ]}
Style={{ maxHeight: '70vh', overflowY: 'auto' }} Style={{maxHeight: '70vh', overflowY: 'auto'}}
> >
{initialData.map((data, index) => ( {initialData.map((data, index) => (
<TopicSingleModifyForm <TopicSingleModifyForm

View File

@@ -16,8 +16,8 @@
*/ */
// TopicSingleModifyForm.js // TopicSingleModifyForm.js
import React, { useEffect } from "react"; import React, {useEffect} from "react";
import {Button, Form, Input, Select, Divider, Row, Col} from "antd"; import {Button, Col, Divider, Form, Input, Row, Select} from "antd";
const TopicSingleModifyForm = ({ const TopicSingleModifyForm = ({
initialData, initialData,
@@ -42,9 +42,9 @@ const TopicSingleModifyForm = ({
const handleFormSubmit = () => { const handleFormSubmit = () => {
form.validateFields() form.validateFields()
.then(values => { .then(values => {
const updatedValues = { ...values }; const updatedValues = {...values};
// 提交时,如果 clusterNameList 或 brokerNameList 为空,则填充所有可用的名称 // 提交时,如果 clusterNameList 或 brokerNameList 为空,则填充所有可用的名称
if(!bIsUpdate){ if (!bIsUpdate) {
if (!updatedValues.clusterNameList || updatedValues.clusterNameList.length === 0) { if (!updatedValues.clusterNameList || updatedValues.clusterNameList.length === 0) {
updatedValues.clusterNameList = allClusterNameList; updatedValues.clusterNameList = allClusterNameList;
} }
@@ -60,29 +60,30 @@ const TopicSingleModifyForm = ({
}; };
const messageTypeOptions = [ const messageTypeOptions = [
{ value: 'TRANSACTION', label: 'TRANSACTION' }, {value: 'TRANSACTION', label: 'TRANSACTION'},
{ value: 'FIFO', label: 'FIFO' }, {value: 'FIFO', label: 'FIFO'},
{ value: 'DELAY', label: 'DELAY' }, {value: 'DELAY', label: 'DELAY'},
{ value: 'NORMAL', label: 'NORMAL' }, {value: 'NORMAL', label: 'NORMAL'},
]; ];
return ( return (
<div style={{ paddingBottom: 24 }}> <div style={{paddingBottom: 24}}>
{bIsUpdate && <Divider orientation="left">{`${t.TOPIC_CONFIG} - ${initialData.brokerNameList ? initialData.brokerNameList.join(', ') : t.UNKNOWN_BROKER}`}</Divider>} {bIsUpdate && <Divider
orientation="left">{`${t.TOPIC_CONFIG} - ${initialData.brokerNameList ? initialData.brokerNameList.join(', ') : t.UNKNOWN_BROKER}`}</Divider>}
<Row justify="center"> {/* 使用 Row 居中内容 */} <Row justify="center"> {/* 使用 Row 居中内容 */}
<Col span={16}> {/* 表单内容占据 12 栅格宽度,并自动居中 */} <Col span={16}> {/* 表单内容占据 12 栅格宽度,并自动居中 */}
<Form <Form
form={form} form={form}
layout="horizontal" layout="horizontal"
labelCol={{ span: 8 }} labelCol={{span: 8}}
wrapperCol={{ span: 16 }} wrapperCol={{span: 16}}
> >
<Form.Item label={t.CLUSTER_NAME} name="clusterNameList"> <Form.Item label={t.CLUSTER_NAME} name="clusterNameList">
<Select <Select
mode="multiple" mode="multiple"
disabled={bIsUpdate} disabled={bIsUpdate}
placeholder={t.SELECT_CLUSTER_NAME} placeholder={t.SELECT_CLUSTER_NAME}
options={allClusterNameList.map(name => ({ value: name, label: name }))} options={allClusterNameList.map(name => ({value: name, label: name}))}
/> />
</Form.Item> </Form.Item>
<Form.Item label="BROKER_NAME" name="brokerNameList"> <Form.Item label="BROKER_NAME" name="brokerNameList">
@@ -90,15 +91,15 @@ const TopicSingleModifyForm = ({
mode="multiple" mode="multiple"
disabled={bIsUpdate} disabled={bIsUpdate}
placeholder={t.SELECT_BROKER_NAME} placeholder={t.SELECT_BROKER_NAME}
options={allBrokerNameList.map(name => ({ value: name, label: name }))} options={allBrokerNameList.map(name => ({value: name, label: name}))}
/> />
</Form.Item> </Form.Item>
<Form.Item <Form.Item
label={t.TOPIC_NAME} label={t.TOPIC_NAME}
name="topicName" name="topicName"
rules={[{ required: true, message: `${t.TOPIC_NAME}${t.CANNOT_BE_EMPTY}` }]} rules={[{required: true, message: `${t.TOPIC_NAME}${t.CANNOT_BE_EMPTY}`}]}
> >
<Input disabled={bIsUpdate} /> <Input disabled={bIsUpdate}/>
</Form.Item> </Form.Item>
<Form.Item label={t.MESSAGE_TYPE} name="messageType"> <Form.Item label={t.MESSAGE_TYPE} name="messageType">
<Select <Select
@@ -109,26 +110,26 @@ const TopicSingleModifyForm = ({
<Form.Item <Form.Item
label={t.WRITE_QUEUE_NUMS} label={t.WRITE_QUEUE_NUMS}
name="writeQueueNums" name="writeQueueNums"
rules={[{ required: true, message: `${t.WRITE_QUEUE_NUMS}${t.CANNOT_BE_EMPTY}` }]} rules={[{required: true, message: `${t.WRITE_QUEUE_NUMS}${t.CANNOT_BE_EMPTY}`}]}
> >
<Input disabled={!writeOperationEnabled} /> <Input disabled={!writeOperationEnabled}/>
</Form.Item> </Form.Item>
<Form.Item <Form.Item
label={t.READ_QUEUE_NUMS} label={t.READ_QUEUE_NUMS}
name="readQueueNums" name="readQueueNums"
rules={[{ required: true, message: `${t.READ_QUEUE_NUMS}${t.CANNOT_BE_EMPTY}` }]} rules={[{required: true, message: `${t.READ_QUEUE_NUMS}${t.CANNOT_BE_EMPTY}`}]}
> >
<Input disabled={!writeOperationEnabled} /> <Input disabled={!writeOperationEnabled}/>
</Form.Item> </Form.Item>
<Form.Item <Form.Item
label={t.PERM} label={t.PERM}
name="perm" name="perm"
rules={[{ required: true, message: `${t.PERM}${t.CANNOT_BE_EMPTY}` }]} rules={[{required: true, message: `${t.PERM}${t.CANNOT_BE_EMPTY}`}]}
> >
<Input disabled={!writeOperationEnabled} /> <Input disabled={!writeOperationEnabled}/>
</Form.Item> </Form.Item>
{!initialData.sysFlag && writeOperationEnabled && ( {!initialData.sysFlag && writeOperationEnabled && (
<Form.Item wrapperCol={{ offset: 8, span: 16 }}> <Form.Item wrapperCol={{offset: 8, span: 16}}>
<Button type="primary" onClick={handleFormSubmit}> <Button type="primary" onClick={handleFormSubmit}>
{t.COMMIT} {t.COMMIT}
</Button> </Button>

View File

@@ -15,20 +15,21 @@
* limitations under the License. * limitations under the License.
*/ */
import React, { createContext, useState, useContext } from 'react'; import React, {createContext, useContext, useState} from 'react';
import { translations } from '../i18n'; import {translations} from '../i18n';
const LanguageContext = createContext({ const LanguageContext = createContext({
lang: 'en', lang: 'en',
setLang: () => {}, setLang: () => {
},
t: translations['en'], // 当前语言的文本资源 t: translations['en'], // 当前语言的文本资源
}); });
export const LanguageProvider = ({ children }) => { export const LanguageProvider = ({children}) => {
const [lang, setLang] = useState('en'); const [lang, setLang] = useState('en');
const t = translations[lang] || translations['en']; const t = translations[lang] || translations['en'];
return ( return (
<LanguageContext.Provider value={{ lang, setLang, t }}> <LanguageContext.Provider value={{lang, setLang, t}}>
{children} {children}
</LanguageContext.Provider> </LanguageContext.Provider>
); );

View File

@@ -47,10 +47,10 @@ export const translations = {
"FETCH_TOPIC_FAILED": "获取主题列表失败", "FETCH_TOPIC_FAILED": "获取主题列表失败",
"CONFIRM_DELETE": "确认删除", "CONFIRM_DELETE": "确认删除",
"CANCEL": "取消", "CANCEL": "取消",
"SELECT_DELETE_BROKERS":"请选择在哪个Broker删除消费者组", "SELECT_DELETE_BROKERS": "请选择在哪个Broker删除消费者组",
"DELETE_CONSUMER_GROUP":"删除消费者组", "DELETE_CONSUMER_GROUP": "删除消费者组",
"ENGLISH": "英文", "ENGLISH": "英文",
"ADD_CONSUMER":"添加消费者", "ADD_CONSUMER": "添加消费者",
"CHINESE": "简体中文", "CHINESE": "简体中文",
"CANNOT_BE_EMPTY": "不能为空", "CANNOT_BE_EMPTY": "不能为空",
"TITLE": "RocketMQ仪表板", "TITLE": "RocketMQ仪表板",
@@ -70,16 +70,16 @@ export const translations = {
"CLUSTER_DETAIL": "集群详情", "CLUSTER_DETAIL": "集群详情",
"COMMIT": "提交", "COMMIT": "提交",
"TOPIC": "主题", "TOPIC": "主题",
"SUBSCRIPTION_GROUP":"订阅组", "SUBSCRIPTION_GROUP": "订阅组",
"PRODUCER_GROUP":"生产组", "PRODUCER_GROUP": "生产组",
"CONSUMER":"消费者", "CONSUMER": "消费者",
"PRODUCER":"生产者", "PRODUCER": "生产者",
"MESSAGE":"消息", "MESSAGE": "消息",
"MESSAGE_DETAIL":"消息详情", "MESSAGE_DETAIL": "消息详情",
"RESEND_MESSAGE":"重新发送", "RESEND_MESSAGE": "重新发送",
"VIEW_EXCEPTION":"查看异常", "VIEW_EXCEPTION": "查看异常",
"DLQ_MESSAGE":"死信消息", "DLQ_MESSAGE": "死信消息",
"MESSAGETRACE":"消息轨迹", "MESSAGETRACE": "消息轨迹",
"OPERATION": "操作", "OPERATION": "操作",
"ADD": "新增", "ADD": "新增",
"UPDATE": "更新", "UPDATE": "更新",
@@ -89,7 +89,7 @@ export const translations = {
"CONFIG": "配置", "CONFIG": "配置",
"SEND_MSG": "发送消息", "SEND_MSG": "发送消息",
"RESET_CUS_OFFSET": "重置消费位点", "RESET_CUS_OFFSET": "重置消费位点",
"SKIP_MESSAGE_ACCUMULATE":"跳过堆积", "SKIP_MESSAGE_ACCUMULATE": "跳过堆积",
"DELETE": "删除", "DELETE": "删除",
"CHANGE_LANG": "更换语言", "CHANGE_LANG": "更换语言",
"CHANGE_VERSION": "更换版本", "CHANGE_VERSION": "更换版本",
@@ -100,73 +100,72 @@ export const translations = {
"TRANSACTION": "事务", "TRANSACTION": "事务",
"UNSPECIFIED": "未指定", "UNSPECIFIED": "未指定",
"DLQ": "死信", "DLQ": "死信",
"QUANTITY":"数量", "QUANTITY": "数量",
"TYPE":"类型", "TYPE": "类型",
"MODE":"模式", "MODE": "模式",
"DELAY":"延迟", "DELAY": "延迟",
"DASHBOARD":"驾驶舱", "DASHBOARD": "驾驶舱",
"CONSUME_DETAIL":"消费详情", "CONSUME_DETAIL": "消费详情",
"CLIENT":"终端", "CLIENT": "终端",
"LAST_CONSUME_TIME":"最后消费时间", "LAST_CONSUME_TIME": "最后消费时间",
"TIME":"时间点", "TIME": "时间点",
"RESET":"重置", "RESET": "重置",
"DATE":"日期", "DATE": "日期",
"NO_DATA":"暂无数据", "NO_DATA": "暂无数据",
"SEARCH":"搜索", "SEARCH": "搜索",
"BEGIN":"开始", "BEGIN": "开始",
"END":"结束", "END": "结束",
"TOPIC_CHANGE":"修改主题", "TOPIC_CHANGE": "修改主题",
"SEND":"发送", "SEND": "发送",
"SUBSCRIPTION_CHANGE":"修改订阅", "SUBSCRIPTION_CHANGE": "修改订阅",
"QUEUE":"队列", "QUEUE": "队列",
"MIN_OFFSET":"最小位点", "MIN_OFFSET": "最小位点",
"MAX_OFFSET":"最大位点", "MAX_OFFSET": "最大位点",
"LAST_UPDATE_TIME_STAMP":"上次更新时间", "LAST_UPDATE_TIME_STAMP": "上次更新时间",
"QUEUE_DATAS":"队列信息", "QUEUE_DATAS": "队列信息",
"READ_QUEUE_NUMS":"读队列数量", "READ_QUEUE_NUMS": "读队列数量",
"WRITE_QUEUE_NUMS":"写队列数量", "WRITE_QUEUE_NUMS": "写队列数量",
"PERM":"perm", "PERM": "perm",
"TAG":"标签", "TAG": "标签",
"KEY":"值", "KEY": "值",
"MESSAGE_BODY":"消息主体", "MESSAGE_BODY": "消息主体",
"TOPIC_NAME":"主题名", "TOPIC_NAME": "主题名",
"ORDER":"顺序", "ORDER": "顺序",
"CONSUMER_CLIENT":"消费者终端", "CONSUMER_CLIENT": "消费者终端",
"BROKER_OFFSET":"代理者位点", "BROKER_OFFSET": "代理者位点",
"CONSUMER_OFFSET":"消费者位点", "CONSUMER_OFFSET": "消费者位点",
"DIFF_TOTAL":"差值", "DIFF_TOTAL": "差值",
"LAST_TIME_STAMP":"上次时间", "LAST_TIME_STAMP": "上次时间",
"RESET_OFFSET":"重置位点", "RESET_OFFSET": "重置位点",
"CLUSTER_NAME":"集群名", "CLUSTER_NAME": "集群名",
"OPS":"运维", "OPS": "运维",
"PROXY":"代理", "PROXY": "代理",
"AUTO_REFRESH":"自动刷新", "AUTO_REFRESH": "自动刷新",
"REFRESH":"刷新", "REFRESH": "刷新",
"LOGOUT":"退出", "LOGOUT": "退出",
"LOGIN":"登录", "LOGIN": "登录",
"USER_NAME":"用户名", "USER_NAME": "用户名",
"PASSWORD":"密码", "PASSWORD": "密码",
"SYSTEM":"系统", "SYSTEM": "系统",
"WELCOME":"您好欢迎使用RocketMQ仪表盘", "WELCOME": "您好欢迎使用RocketMQ仪表盘",
"ENABLE_MESSAGE_TRACE":"开启消息轨迹", "ENABLE_MESSAGE_TRACE": "开启消息轨迹",
"MESSAGE_TRACE_DETAIL":"消息轨迹详情", "MESSAGE_TRACE_DETAIL": "消息轨迹详情",
"TRACE_TOPIC":"消息轨迹主题", "TRACE_TOPIC": "消息轨迹主题",
"SELECT_TRACE_TOPIC":"选择消息轨迹主题", "SELECT_TRACE_TOPIC": "选择消息轨迹主题",
"EXPORT": "导出", "EXPORT": "导出",
"NO_MATCH_RESULT": "没有查到符合条件的结果", "NO_MATCH_RESULT": "没有查到符合条件的结果",
"BATCH_RESEND": "批量重发", "BATCH_RESEND": "批量重发",
"BATCH_EXPORT": "批量导出", "BATCH_EXPORT": "批量导出",
"WHITE_LIST":"白名单", "ACCOUNT_INFO": "账户信息",
"ACCOUNT_INFO":"账户信息", "IS_ADMIN": "是否管理员",
"IS_ADMIN":"是否管理员", "DEFAULT_TOPIC_PERM": "topic默认权限",
"DEFAULT_TOPIC_PERM":"topic默认权限", "DEFAULT_GROUP_PERM": "消费组默认权限",
"DEFAULT_GROUP_PERM":"消费组默认权限", "TOPIC_PERM": "topic权限",
"TOPIC_PERM":"topic权限", "GROUP_PERM": "消费组权限",
"GROUP_PERM":"消费组权限", "SYNCHRONIZE": "同步",
"SYNCHRONIZE":"同步", "SHOW": "显示",
"SHOW":"显示", "HIDE": "隐藏",
"HIDE":"隐藏", "MESSAGE_TYPE": "消息类型",
"MESSAGE_TYPE":"消息类型",
"MESSAGE_TYPE_UNSPECIFIED": "未指定,为普通消息", "MESSAGE_TYPE_UNSPECIFIED": "未指定,为普通消息",
"MESSAGE_TYPE_NORMAL": "普通消息", "MESSAGE_TYPE_NORMAL": "普通消息",
"MESSAGE_TYPE_FIFO": "顺序消息", "MESSAGE_TYPE_FIFO": "顺序消息",
@@ -279,6 +278,12 @@ export const translations = {
"ENTER_IP_HINT": "请输入 IP 地址,按回车键添加,支持 IPv4、IPv6 和 CIDR", "ENTER_IP_HINT": "请输入 IP 地址,按回车键添加,支持 IPv4、IPv6 和 CIDR",
"PLEASE_ENTER_DECISION": "请输入决策!", "PLEASE_ENTER_DECISION": "请输入决策!",
"MENU": "菜单", "MENU": "菜单",
"SELECT_PROXY": "选择代理",
"ENABLE_PROXY": "启用代理",
"PROXY_DISABLED": "代理禁用",
"PROXY_ENABLED": "代理启用",
"BROKER_OVERVIEW": "Broker概览",
"TOTAL_MSG_RECEIVED_TODAY": "今天接收的总消息数",
}, },
en: { en: {
"DEFAULT": "Default", "DEFAULT": "Default",
@@ -294,7 +299,7 @@ export const translations = {
"SELECT_TOPIC_PLACEHOLDER": "Please select topic", "SELECT_TOPIC_PLACEHOLDER": "Please select topic",
"MESSAGE_ID_TOPIC_HINT": "Message ID Topic", "MESSAGE_ID_TOPIC_HINT": "Message ID Topic",
"TOPIC_ADD": "Add Topic", "TOPIC_ADD": "Add Topic",
"SKIP_MESSAGE_ACCUMULATE":"Skip Message Accumulate", "SKIP_MESSAGE_ACCUMULATE": "Skip Message Accumulate",
"OPERATION_FAILED": "Operation Failed", "OPERATION_FAILED": "Operation Failed",
"FORM_VALIDATION_FAILED": "Form Validation Failed", "FORM_VALIDATION_FAILED": "Form Validation Failed",
"ADD_CONSUMER": "Add Consumer", "ADD_CONSUMER": "Add Consumer",
@@ -325,7 +330,7 @@ export const translations = {
"ADDRESS": "Address", "ADDRESS": "Address",
"VERSION": "Version", "VERSION": "Version",
"PRO_MSG_TPS": "Produce Message TPS", "PRO_MSG_TPS": "Produce Message TPS",
"CUS_MSG_TPS": "Consume Message TPS", "CUS_MSG_TPS": "Consumer Message TPS",
"YESTERDAY_PRO_COUNT": "Yesterday Produce Count", "YESTERDAY_PRO_COUNT": "Yesterday Produce Count",
"YESTERDAY_CUS_COUNT": "Yesterday Consume Count", "YESTERDAY_CUS_COUNT": "Yesterday Consume Count",
"TODAY_PRO_COUNT": "Today Produce Count", "TODAY_PRO_COUNT": "Today Produce Count",
@@ -335,16 +340,16 @@ export const translations = {
"CLUSTER": "Cluster", "CLUSTER": "Cluster",
"CLUSTER_DETAIL": "Cluster Detail", "CLUSTER_DETAIL": "Cluster Detail",
"TOPIC": "Topic", "TOPIC": "Topic",
"SUBSCRIPTION_GROUP":"SubscriptionGroup", "SUBSCRIPTION_GROUP": "SubscriptionGroup",
"PRODUCER_GROUP":"ProducerGroup", "PRODUCER_GROUP": "ProducerGroup",
"CONSUMER":"Consumer", "CONSUMER": "Consumer",
"PRODUCER":"Producer", "PRODUCER": "Producer",
"MESSAGE":"Message", "MESSAGE": "Message",
"MESSAGE_DETAIL":"Message Detail", "MESSAGE_DETAIL": "Message Detail",
"RESEND_MESSAGE":"Resend Message", "RESEND_MESSAGE": "Resend Message",
"VIEW_EXCEPTION":"View Exception", "VIEW_EXCEPTION": "View Exception",
"MESSAGETRACE":"MessageTrace", "MESSAGETRACE": "MessageTrace",
"DLQ_MESSAGE":"DLQMessage", "DLQ_MESSAGE": "DLQMessage",
"COMMIT": "Commit", "COMMIT": "Commit",
"OPERATION": "Operation", "OPERATION": "Operation",
"ADD": "Add", "ADD": "Add",
@@ -365,73 +370,72 @@ export const translations = {
"TRANSACTION": "TRANSACTION", "TRANSACTION": "TRANSACTION",
"UNSPECIFIED": "UNSPECIFIED", "UNSPECIFIED": "UNSPECIFIED",
"DLQ": "DLQ", "DLQ": "DLQ",
"QUANTITY":"Quantity", "QUANTITY": "Quantity",
"TYPE":"Type", "TYPE": "Type",
"MODE":"Mode", "MODE": "Mode",
"DELAY":"Delay", "DELAY": "Delay",
"DASHBOARD":"Dashboard", "DASHBOARD": "Dashboard",
"CONSUME_DETAIL":"CONSUME DETAIL", "CONSUME_DETAIL": "CONSUME DETAIL",
"CLIENT":"CLIENT", "CLIENT": "CLIENT",
"LAST_CONSUME_TIME":"LastConsumeTime", "LAST_CONSUME_TIME": "LastConsumeTime",
"TIME":"Time", "TIME": "Time",
"RESET":"RESET", "RESET": "RESET",
"DATE":"Date", "DATE": "Date",
"NO_DATA":"NO DATA", "NO_DATA": "NO DATA",
"SEARCH":"Search", "SEARCH": "Search",
"BEGIN":"Begin", "BEGIN": "Begin",
"END":"End", "END": "End",
"TOPIC_CHANGE":"Topic Change", "TOPIC_CHANGE": "Topic Change",
"SEND":"Send", "SEND": "Send",
"SUBSCRIPTION_CHANGE":"Subscription Change", "SUBSCRIPTION_CHANGE": "Subscription Change",
"QUEUE":"Queue", "QUEUE": "Queue",
"MIN_OFFSET":"minOffset", "MIN_OFFSET": "minOffset",
"MAX_OFFSET":"maxOffset", "MAX_OFFSET": "maxOffset",
"LAST_UPDATE_TIME_STAMP":"lastUpdateTimeStamp", "LAST_UPDATE_TIME_STAMP": "lastUpdateTimeStamp",
"QUEUE_DATAS":"queueDatas", "QUEUE_DATAS": "queueDatas",
"READ_QUEUE_NUMS":"readQueueNums", "READ_QUEUE_NUMS": "readQueueNums",
"WRITE_QUEUE_NUMS":"writeQueueNums", "WRITE_QUEUE_NUMS": "writeQueueNums",
"PERM":"perm", "PERM": "perm",
"TAG":"Tag", "TAG": "Tag",
"KEY":"Key", "KEY": "Key",
"MESSAGE_BODY":"Message Body", "MESSAGE_BODY": "Message Body",
"TOPIC_NAME":"topicName", "TOPIC_NAME": "topicName",
"ORDER":"order", "ORDER": "order",
"CONSUMER_CLIENT":"consumerClient", "CONSUMER_CLIENT": "consumerClient",
"BROKER_OFFSET":"brokerOffset", "BROKER_OFFSET": "brokerOffset",
"CONSUMER_OFFSET":"consumerOffset", "CONSUMER_OFFSET": "consumerOffset",
"DIFF_TOTAL":"diffTotal", "DIFF_TOTAL": "diffTotal",
"LAST_TIME_STAMP":"lastTimeStamp", "LAST_TIME_STAMP": "lastTimeStamp",
"RESET_OFFSET":"resetOffset", "RESET_OFFSET": "resetOffset",
"CLUSTER_NAME":"clusterName", "CLUSTER_NAME": "clusterName",
"OPS":"OPS", "OPS": "OPS",
"PROXY":"Proxy", "PROXY": "Proxy",
"AUTO_REFRESH":"AUTO_REFRESH", "AUTO_REFRESH": "AUTO_REFRESH",
"REFRESH":"REFRESH", "REFRESH": "REFRESH",
"LOGOUT":"Logout", "LOGOUT": "Logout",
"LOGIN":"Login", "LOGIN": "Login",
"USER_NAME":"Username", "USER_NAME": "Username",
"PASSWORD":"Password", "PASSWORD": "Password",
"SYSTEM":"SYSTEM", "SYSTEM": "SYSTEM",
"WELCOME":"Hi, welcome using RocketMQ Dashboard", "WELCOME": "Hi, welcome using RocketMQ Dashboard",
"ENABLE_MESSAGE_TRACE":"Enable Message Trace", "ENABLE_MESSAGE_TRACE": "Enable Message Trace",
"MESSAGE_TRACE_DETAIL":"Message Trace Detail", "MESSAGE_TRACE_DETAIL": "Message Trace Detail",
"TRACE_TOPIC":"TraceTopic", "TRACE_TOPIC": "TraceTopic",
"SELECT_TRACE_TOPIC":"selectTraceTopic", "SELECT_TRACE_TOPIC": "selectTraceTopic",
"EXPORT": "export", "EXPORT": "export",
"NO_MATCH_RESULT": "no match result", "NO_MATCH_RESULT": "no match result",
"BATCH_RESEND": "batchReSend", "BATCH_RESEND": "batchReSend",
"BATCH_EXPORT": "batchExport", "BATCH_EXPORT": "batchExport",
"WHITE_LIST":"White List", "ACCOUNT_INFO": "Account Info",
"ACCOUNT_INFO":"Account Info", "IS_ADMIN": "Is Admin",
"IS_ADMIN":"Is Admin", "DEFAULT_TOPIC_PERM": "Default Topic Permission",
"DEFAULT_TOPIC_PERM":"Default Topic Permission", "DEFAULT_GROUP_PERM": "Default Group Permission",
"DEFAULT_GROUP_PERM":"Default Group Permission", "TOPIC_PERM": "Topic Permission",
"TOPIC_PERM":"Topic Permission", "GROUP_PERM": "Group Permission",
"GROUP_PERM":"Group Permission", "SYNCHRONIZE": "Synchronize Data",
"SYNCHRONIZE":"Synchronize Data", "SHOW": "Show",
"SHOW":"Show", "HIDE": "Hide",
"HIDE":"Hide", "MESSAGE_TYPE": "messageType",
"MESSAGE_TYPE":"messageType",
"MESSAGE_TYPE_UNSPECIFIED": "UNSPECIFIED, is NORMAL", "MESSAGE_TYPE_UNSPECIFIED": "UNSPECIFIED, is NORMAL",
"MESSAGE_TYPE_NORMAL": "NORMAL", "MESSAGE_TYPE_NORMAL": "NORMAL",
"MESSAGE_TYPE_FIFO": "FIFO", "MESSAGE_TYPE_FIFO": "FIFO",
@@ -536,6 +540,13 @@ export const translations = {
"ENTER_IP_HINT": "Please enter IP address, press Enter to add. Supports IPv4, IPv6, and CIDR.", "ENTER_IP_HINT": "Please enter IP address, press Enter to add. Supports IPv4, IPv6, and CIDR.",
"PLEASE_ENTER_DECISION": "Please enter decision!", "PLEASE_ENTER_DECISION": "Please enter decision!",
"MENU": "Menu", "MENU": "Menu",
"SELECT_PROXY": "Select Proxy",
"ENABLE_PROXY": "Enable Proxy",
"PROXY_DISABLED": "Proxy Disabled",
"PROXY_ENABLED": "Proxy Enabled",
"BROKER_OVERVIEW": "Broker Overview",
"TOTAL_MSG_RECEIVED_TODAY": "Total messages received today",
} }

View File

@@ -19,7 +19,7 @@ import React from 'react';
import ReactDOM from 'react-dom/client'; import ReactDOM from 'react-dom/client';
import './index.css'; import './index.css';
import App from './App'; import App from './App';
import { App as AntdApp } from 'antd'; import {App as AntdApp} from 'antd';
import reportWebVitals from './reportWebVitals'; import reportWebVitals from './reportWebVitals';
import {LanguageProvider} from "./i18n/LanguageContext"; import {LanguageProvider} from "./i18n/LanguageContext";
import {Provider} from "react-redux"; import {Provider} from "react-redux";
@@ -27,7 +27,6 @@ import store from './store';
const root = ReactDOM.createRoot(document.getElementById('root')); const root = ReactDOM.createRoot(document.getElementById('root'));
root.render( root.render(
<LanguageProvider> <LanguageProvider>
<React.StrictMode> <React.StrictMode>
<AntdApp> <AntdApp>
@@ -37,7 +36,6 @@ root.render(
</AntdApp> </AntdApp>
</React.StrictMode> </React.StrictMode>
</LanguageProvider> </LanguageProvider>
); );
// If you want to start measuring performance in your app, pass a function // If you want to start measuring performance in your app, pass a function

View File

@@ -16,7 +16,7 @@
*/ */
import React, {useCallback, useEffect, useState} from 'react'; import React, {useCallback, useEffect, useState} from 'react';
import {Button, Checkbox, Input, message, notification, Spin, Table} from 'antd'; import {Button, Checkbox, Input, message, notification, Select, Spin, Switch, Table} from 'antd';
import {useLanguage} from '../../i18n/LanguageContext'; import {useLanguage} from '../../i18n/LanguageContext';
import {remoteApi} from '../../api/remoteApi/remoteApi'; import {remoteApi} from '../../api/remoteApi/remoteApi';
import ClientInfoModal from "../../components/consumer/ClientInfoModal"; import ClientInfoModal from "../../components/consumer/ClientInfoModal";
@@ -44,8 +44,29 @@ const ConsumerGroupList = () => {
const [isAddConfig, setIsAddConfig] = useState(false); const [isAddConfig, setIsAddConfig] = useState(false);
const [showDeleteModal, setShowDeleteModal] = useState(false); const [showDeleteModal, setShowDeleteModal] = useState(false);
const [messageApi, msgContextHolder] = message.useMessage(); const [messageApi, msgContextHolder] = message.useMessage();
const [notificationApi,notificationContextHolder] = notification.useNotification(); const [notificationApi, notificationContextHolder] = notification.useNotification();
const [proxyEnabled, setProxyEnabled] = useState(() => {
try {
const storedValue = localStorage.getItem('proxyEnabled');
return storedValue ? JSON.parse(storedValue) : false;
} catch (error) {
console.error("Failed to read proxyEnabled from localStorage:", error);
return false;
}
});
const [selectedProxy, setSelectedProxy] = useState(() => {
try {
const storedValue = localStorage.getItem('selectedProxy');
return storedValue || undefined;
} catch (error) {
console.error("Failed to read selectedProxy from localStorage:", error);
return undefined;
}
});
const [proxyOptions ,setProxyOptions]= useState([]);
const [paginationConf, setPaginationConf] = useState({ const [paginationConf, setPaginationConf] = useState({
current: 1, current: 1,
pageSize: 10, pageSize: 10,
@@ -60,12 +81,17 @@ const ConsumerGroupList = () => {
const loadConsumerGroups = useCallback(async (currentPage) => { const loadConsumerGroups = useCallback(async (currentPage) => {
setLoading(true); setLoading(true);
try { try {
const response = await remoteApi.queryConsumerGroupList(false); var response;
if(!proxyEnabled){
response = await remoteApi.queryConsumerGroupList(false);
}else{
response = await remoteApi.queryConsumerGroupList(false, selectedProxy);
}
if (response.status === 0) { if (response.status === 0) {
setAllConsumerGroupList(response.data); setAllConsumerGroupList(response.data);
if(currentPage!=null){ if (currentPage != null) {
filterList(currentPage, response.data); filterList(currentPage, response.data);
}else{ } else {
filterList(1, response.data); filterList(1, response.data);
} }
} else { } else {
@@ -87,7 +113,6 @@ const ConsumerGroupList = () => {
}; };
const filterList = useCallback((currentPage, data) => { const filterList = useCallback((currentPage, data) => {
// 排序处理
let sortedData = [...data]; let sortedData = [...data];
if (sortConfig.sortKey) { if (sortConfig.sortKey) {
sortedData.sort((a, b) => { sortedData.sort((a, b) => {
@@ -153,6 +178,48 @@ const ConsumerGroupList = () => {
filterList(paginationConf.current, sortedList); filterList(paginationConf.current, sortedList);
}, [sortConfig, allConsumerGroupList, paginationConf.current]); }, [sortConfig, allConsumerGroupList, paginationConf.current]);
const fetchProxyList = useCallback(async () => {
remoteApi.queryProxyHomePage((resp) => {
setLoading(false);
if (resp.status === 0) {
const {proxyAddrList, currentProxyAddr} = resp.data;
const options = proxyAddrList.map(proxyAddress => ({
label: proxyAddress,
value: proxyAddress,
}));
setProxyOptions(options || []);
setSelectedProxy(prevSelectedProxy => {
if (prevSelectedProxy) {
return prevSelectedProxy;
}
if (options.length > 0) {
return options[0].value;
}
return undefined;
});
} else {
notificationApi.error({message: resp.errMsg || t.FETCH_PROXY_LIST_FAILED, duration: 2});
}
});
}, [t]);
useEffect(() => {
localStorage.setItem('proxyEnabled', JSON.stringify(proxyEnabled));
}, [proxyEnabled]);
useEffect(() => {
if (selectedProxy) {
localStorage.setItem('selectedProxy', selectedProxy);
} else {
localStorage.removeItem('selectedProxy');
}
}, [selectedProxy]);
useEffect(() => {
fetchProxyList();
}, []);
useEffect(() => { useEffect(() => {
loadConsumerGroups(); loadConsumerGroups();
}, [loadConsumerGroups]); }, [loadConsumerGroups]);
@@ -380,7 +447,7 @@ const ConsumerGroupList = () => {
filterList(pagination.current, allConsumerGroupList); filterList(pagination.current, allConsumerGroupList);
}; };
const closeConfigModal = () =>{ const closeConfigModal = () => {
setShowConfig(false); setShowConfig(false);
setIsAddConfig(false); setIsAddConfig(false);
} }
@@ -389,16 +456,18 @@ const ConsumerGroupList = () => {
<> <>
{msgContextHolder} {msgContextHolder}
{notificationContextHolder} {notificationContextHolder}
<div style={{padding: '20px'}}> <div style={{ padding: '20px' }}>
<Spin spinning={loading} tip={t.LOADING}> <Spin spinning={loading} tip={t.LOADING}>
<div style={{marginBottom: '20px'}}> <div style={{ marginBottom: '20px', display: 'flex', justifyContent: 'space-between', alignItems: 'center' }}>
<div style={{display: 'flex', alignItems: 'center', gap: '15px'}}> {/* 左侧:筛选和操作按钮 */}
<div style={{display: 'flex', alignItems: 'center'}}> <div style={{ display: 'flex', alignItems: 'center', gap: '15px', flexWrap: 'wrap' }}>
<label style={{marginRight: '8px'}}>{t.SUBSCRIPTION_GROUP}:</label> <div style={{ display: 'flex', alignItems: 'center' }}>
<label style={{ marginRight: '8px', whiteSpace: 'nowrap' }}>{t.SUBSCRIPTION_GROUP}:</label>
<Input <Input
style={{width: '200px'}} style={{ width: '200px' }}
value={filterStr} value={filterStr}
onChange={(e) => handleFilterInputChange(e.target.value)} onChange={(e) => handleFilterInputChange(e.target.value)}
placeholder="输入订阅组名称"
/> />
</div> </div>
<Checkbox checked={filterNormal} <Checkbox checked={filterNormal}
@@ -423,12 +492,35 @@ const ConsumerGroupList = () => {
<Button type="primary" onClick={handleRefreshConsumerData}> <Button type="primary" onClick={handleRefreshConsumerData}>
{t.REFRESH} {t.REFRESH}
</Button> </Button>
{/*<Switch*/} </div>
{/* checked={intervalProcessSwitch}*/}
{/* onChange={(checked) => setIntervalProcessSwitch(checked)}*/} {/* 右侧:代理选项 */}
{/* checkedChildren={t.AUTO_REFRESH}*/} <div style={{ display: 'flex', alignItems: 'center', gap: '15px' }}>
{/* unCheckedChildren={t.AUTO_REFRESH}*/} <label style={{ marginRight: '8px', whiteSpace: 'nowrap' }}>{t.SELECT_PROXY}:</label>
{/*/>*/} <Select
style={{ width: '220px' }}
placeholder={t.SELECT_PROXY}
onChange={(value) => setSelectedProxy(value)}
value={selectedProxy}
options={proxyOptions}
disabled={!proxyEnabled}
allowClear
/>
<label style={{ marginRight: '8px', whiteSpace: 'nowrap' }}>{t.ENABLE_PROXY}:</label>
<Switch
checked={proxyEnabled}
onChange={(checked) => {
setProxyEnabled(checked);
if (!checked) {
setSelectedProxy(undefined);
messageApi.info(t.PROXY_DISABLED);
} else {
messageApi.info(t.PROXY_ENABLED);
}
}}
checkedChildren={t.ENABLED}
unCheckedChildren={t.DISABLED}
/>
</div> </div>
</div> </div>
@@ -443,6 +535,7 @@ const ConsumerGroupList = () => {
/> />
</Spin> </Spin>
{/* 模态框组件保持不变 */}
<ClientInfoModal <ClientInfoModal
visible={showClientInfo} visible={showClientInfo}
group={selectedGroup} group={selectedGroup}

View File

@@ -250,17 +250,18 @@ const DashboardPage = () => {
const brokerAddrTable = resp.data.clusterInfo.brokerAddrTable; // Corrected to brokerAddrTable const brokerAddrTable = resp.data.clusterInfo.brokerAddrTable; // Corrected to brokerAddrTable
const brokerDetail = resp.data.brokerServer; const brokerDetail = resp.data.brokerServer;
const clusterMap = tools.generateBrokerMap(brokerDetail, clusterAddrTable, brokerAddrTable); const clusterMap = tools.generateBrokerMap(brokerDetail, clusterAddrTable, brokerAddrTable);
console.log(brokerAddrTable)
let brokerArray = []; let brokerArray = [];
Object.values(clusterMap).forEach(brokersInCluster => { Object.values(clusterMap).forEach(brokersInCluster => {
brokerArray = brokerArray.concat(brokersInCluster); brokerArray = brokerArray.concat(brokersInCluster);
}); });
// Update broker table data const newData = brokerArray.map(broker => ({
setBrokerTableData(brokerArray.map(broker => ({
...broker, ...broker,
key: broker.brokerName // Ant Design Table needs a unique key key: broker.brokerName,
}))); }));
console.log("即将设置的数据:", newData); // 先打印
setBrokerTableData(newData); // 再设置状态
brokerArray.sort((firstBroker, lastBroker) => { brokerArray.sort((firstBroker, lastBroker) => {
const firstTotalMsg = parseFloat(firstBroker.msgGetTotalTodayNow || 0); const firstTotalMsg = parseFloat(firstBroker.msgGetTotalTodayNow || 0);
@@ -347,7 +348,7 @@ const DashboardPage = () => {
const brokerColumns = [ const brokerColumns = [
{title: t.BROKER_NAME, dataIndex: 'brokerName', key: 'brokerName'}, {title: t.BROKER_NAME, dataIndex: 'brokerName', key: 'brokerName'},
{title: t.BROKER_ADDR, dataIndex: 'brokerAddress', key: 'brokerAddress'}, {title: t.BROKER_ADDR, dataIndex: 'address', key: 'address'},
{ {
title: t.TOTAL_MSG_RECEIVED_TODAY, title: t.TOTAL_MSG_RECEIVED_TODAY,
dataIndex: 'msgGetTotalTodayNow', dataIndex: 'msgGetTotalTodayNow',

View File

@@ -179,7 +179,6 @@ const DlqMessageQueryPage = () => {
return; return;
} }
setLoading(true); setLoading(true);
// console.log("根据Message ID查询DLQ消息:", { msgId: messageId, consumerGroup: selectedConsumerGroup });
try { try {
const resp = await remoteApi.viewMessage(messageId, DLQ_GROUP_TOPIC_PREFIX + selectedConsumerGroup); const resp = await remoteApi.viewMessage(messageId, DLQ_GROUP_TOPIC_PREFIX + selectedConsumerGroup);
if (resp.status === 0) { if (resp.status === 0) {
@@ -323,7 +322,6 @@ const DlqMessageQueryPage = () => {
msgId: message.properties.ORIGIN_MESSAGE_ID, msgId: message.properties.ORIGIN_MESSAGE_ID,
consumerGroup: selectedConsumerGroup, consumerGroup: selectedConsumerGroup,
})); }));
// console.log(`批量重发DLQ消息到 ${selectedConsumerGroup}:`, messagesToResend);
try { try {
const resp = await remoteApi.batchResendDlqMessage(messagesToResend); const resp = await remoteApi.batchResendDlqMessage(messagesToResend);
if (resp.status === 0) { if (resp.status === 0) {
@@ -355,7 +353,6 @@ const DlqMessageQueryPage = () => {
message: t.ERROR, message: t.ERROR,
description: t.BATCH_RESEND_FAILED, description: t.BATCH_RESEND_FAILED,
}); });
console.error("批量重发失败:", error);
} finally { } finally {
setLoading(false); setLoading(false);
} }

View File

@@ -16,17 +16,17 @@
*/ */
import React from 'react'; import React from 'react';
import { Form, Input, Button, message, Typography } from 'antd'; import {Button, Form, Input, message, Typography} from 'antd';
import {remoteApi} from "../../api/remoteApi/remoteApi"; import {remoteApi} from "../../api/remoteApi/remoteApi";
const { Title } = Typography; const {Title} = Typography;
const Login = () => { const Login = () => {
const [form] = Form.useForm(); const [form] = Form.useForm();
const [messageApi, msgContextHolder] = message.useMessage(); const [messageApi, msgContextHolder] = message.useMessage();
const onFinish = async (values) => { const onFinish = async (values) => {
const { username, password } = values; const {username, password} = values;
remoteApi.login(username, password).then((res) => { remoteApi.login(username, password).then((res) => {
if (res.status === 0) { if (res.status === 0) {
messageApi.success('登录成功'); messageApi.success('登录成功');

View File

@@ -146,7 +146,6 @@ const MessageQueryPage = () => {
message: t.ERROR, message: t.ERROR,
description: t.QUERY_FAILED, description: t.QUERY_FAILED,
}); });
console.error("查询失败:", error);
} finally { } finally {
setLoading(false); setLoading(false);
} }
@@ -182,7 +181,6 @@ const MessageQueryPage = () => {
message: t.ERROR, message: t.ERROR,
description: t.QUERY_FAILED, description: t.QUERY_FAILED,
}); });
console.error("查询失败:", error);
} finally { } finally {
setLoading(false); setLoading(false);
} }
@@ -241,7 +239,6 @@ const MessageQueryPage = () => {
message: t.ERROR, message: t.ERROR,
description: t.RESEND_FAILED, description: t.RESEND_FAILED,
}); });
console.error("重发失败:", error);
} finally { } finally {
setLoading(false); setLoading(false);
// Optionally, you might want to refresh the message detail after resend // Optionally, you might want to refresh the message detail after resend
@@ -455,7 +452,6 @@ const MessageQueryPage = () => {
</Button> </Button>
</Form.Item> </Form.Item>
</Form> </Form>
{/* Message ID 查询结果通常直接弹窗显示,这里不需要表格 */}
</div> </div>
</TabPane> </TabPane>
</Tabs> </Tabs>

View File

@@ -15,12 +15,12 @@
* limitations under the License. * limitations under the License.
*/ */
import React, { useState, useEffect } from 'react'; import React, {useEffect, useState} from 'react';
import { Select, Button, Switch, Input, Typography, Space, message } from 'antd'; import {Button, Input, message, Select, Space, Switch, Typography} from 'antd';
import {remoteApi} from '../../api/remoteApi/remoteApi'; import {remoteApi} from '../../api/remoteApi/remoteApi';
const { Title } = Typography; const {Title} = Typography;
const { Option } = Select; const {Option} = Select;
const Ops = () => { const Ops = () => {
const [namesrvAddrList, setNamesrvAddrList] = useState([]); const [namesrvAddrList, setNamesrvAddrList] = useState([]);

View File

@@ -15,16 +15,16 @@
* limitations under the License. * limitations under the License.
*/ */
import React, { useState, useEffect } from 'react'; import React, {useEffect, useState} from 'react';
import { Modal, Button, Select, Input, Card, Row, Col, notification, Spin } from 'antd'; import {Button, Card, Col, Input, Modal, notification, Row, Select, Spin} from 'antd';
import { useLanguage } from '../../i18n/LanguageContext'; import {useLanguage} from '../../i18n/LanguageContext';
import { remoteApi } from "../../api/remoteApi/remoteApi"; import {remoteApi} from "../../api/remoteApi/remoteApi";
const { Option } = Select; const {Option} = Select;
const ProxyManager = () => { const ProxyManager = () => {
const { t } = useLanguage(); const {t} = useLanguage();
const [loading, setLoading] = useState(false); const [loading, setLoading] = useState(false);
const [proxyAddrList, setProxyAddrList] = useState([]); const [proxyAddrList, setProxyAddrList] = useState([]);
@@ -47,7 +47,7 @@ const ProxyManager = () => {
remoteApi.queryProxyHomePage((resp) => { remoteApi.queryProxyHomePage((resp) => {
setLoading(false); setLoading(false);
if (resp.status === 0) { if (resp.status === 0) {
const { proxyAddrList, currentProxyAddr } = resp.data; const {proxyAddrList, currentProxyAddr} = resp.data;
setProxyAddrList(proxyAddrList || []); setProxyAddrList(proxyAddrList || []);
setSelectedProxy(currentProxyAddr || (proxyAddrList && proxyAddrList.length > 0 ? proxyAddrList[0] : '')); setSelectedProxy(currentProxyAddr || (proxyAddrList && proxyAddrList.length > 0 ? proxyAddrList[0] : ''));
@@ -58,7 +58,7 @@ const ProxyManager = () => {
} }
} else { } else {
notificationApi.error({ message: resp.errMsg || t.FETCH_PROXY_LIST_FAILED, duration: 2 }); notificationApi.error({message: resp.errMsg || t.FETCH_PROXY_LIST_FAILED, duration: 2});
} }
}); });
}, [t]); }, [t]);
@@ -71,7 +71,10 @@ const ProxyManager = () => {
const handleAddProxyAddr = () => { const handleAddProxyAddr = () => {
if (!newProxyAddr.trim()) { if (!newProxyAddr.trim()) {
notificationApi.warning({ message: t.INPUT_PROXY_ADDR_REQUIRED || "Please input a new proxy address.", duration: 2 }); notificationApi.warning({
message: t.INPUT_PROXY_ADDR_REQUIRED || "Please input a new proxy address.",
duration: 2
});
return; return;
} }
setLoading(true); setLoading(true);
@@ -82,28 +85,28 @@ const ProxyManager = () => {
setProxyAddrList(prevList => [...prevList, newProxyAddr.trim()]); setProxyAddrList(prevList => [...prevList, newProxyAddr.trim()]);
} }
setNewProxyAddr(''); setNewProxyAddr('');
notificationApi.info({ message: t.SUCCESS || "SUCCESS", duration: 2 }); notificationApi.info({message: t.SUCCESS || "SUCCESS", duration: 2});
} else { } else {
notificationApi.error({ message: resp.errMsg || t.ADD_PROXY_FAILED, duration: 2 }); notificationApi.error({message: resp.errMsg || t.ADD_PROXY_FAILED, duration: 2});
} }
}); });
}; };
return ( return (
<Spin spinning={loading} tip={t.LOADING}> <Spin spinning={loading} tip={t.LOADING}>
<div className="container-fluid" style={{ padding: '24px' }} id="deployHistoryList"> <div className="container-fluid" style={{padding: '24px'}} id="deployHistoryList">
<Card <Card
title={ title={
<div style={{ fontSize: '20px', fontWeight: 'bold' }}> <div style={{fontSize: '20px', fontWeight: 'bold'}}>
ProxyServerAddressList ProxyServerAddressList
</div> </div>
} }
bordered={false} bordered={false}
> >
<Row gutter={[16, 16]} align="middle"> <Row gutter={[16, 16]} align="middle">
<Col flex="auto" style={{ minWidth: 300, maxWidth: 500 }}> <Col flex="auto" style={{minWidth: 300, maxWidth: 500}}>
<Select <Select
style={{ width: '100%' }} style={{width: '100%'}}
value={selectedProxy} value={selectedProxy}
onChange={handleSelectChange} onChange={handleSelectChange}
placeholder={t.SELECT} placeholder={t.SELECT}
@@ -122,14 +125,14 @@ const ProxyManager = () => {
</Row> </Row>
{writeOperationEnabled && ( {writeOperationEnabled && (
<Row gutter={[16, 16]} align="middle" style={{ marginTop: 16 }}> <Row gutter={[16, 16]} align="middle" style={{marginTop: 16}}>
<Col> <Col>
<label htmlFor="newProxyAddrInput">ProxyAddr:</label> <label htmlFor="newProxyAddrInput">ProxyAddr:</label>
</Col> </Col>
<Col> <Col>
<Input <Input
id="newProxyAddrInput" id="newProxyAddrInput"
style={{ width: 300 }} style={{width: 300}}
value={newProxyAddr} value={newProxyAddr}
onChange={(e) => setNewProxyAddr(e.target.value)} onChange={(e) => setNewProxyAddr(e.target.value)}
placeholder={t.INPUT_PROXY_ADDR} placeholder={t.INPUT_PROXY_ADDR}
@@ -149,25 +152,26 @@ const ProxyManager = () => {
onCancel={() => setShowModal(false)} onCancel={() => setShowModal(false)}
title={`${t.PROXY_CONFIG} [${selectedProxy}]`} title={`${t.PROXY_CONFIG} [${selectedProxy}]`}
footer={ footer={
<div style={{ textAlign: 'center' }}> <div style={{textAlign: 'center'}}>
<Button onClick={() => setShowModal(false)}>{t.CLOSE}</Button> <Button onClick={() => setShowModal(false)}>{t.CLOSE}</Button>
</div> </div>
} }
width={800} width={800}
bodyStyle={{ maxHeight: '60vh', overflowY: 'auto' }} bodyStyle={{maxHeight: '60vh', overflowY: 'auto'}}
> >
<table className="table table-bordered" style={{ width: '100%' }}> <table className="table table-bordered" style={{width: '100%'}}>
<tbody> <tbody>
{Object.entries(allProxyConfig).length > 0 ? ( {Object.entries(allProxyConfig).length > 0 ? (
Object.entries(allProxyConfig).map(([key, value]) => ( Object.entries(allProxyConfig).map(([key, value]) => (
<tr key={key}> <tr key={key}>
<td style={{ fontWeight: 500, width: '30%' }}>{key}</td> <td style={{fontWeight: 500, width: '30%'}}>{key}</td>
<td>{value}</td> <td>{value}</td>
</tr> </tr>
)) ))
) : ( ) : (
<tr> <tr>
<td colSpan="2" style={{ textAlign: 'center' }}>{t.NO_CONFIG_DATA || "No configuration data available."}</td> <td colSpan="2"
style={{textAlign: 'center'}}>{t.NO_CONFIG_DATA || "No configuration data available."}</td>
</tr> </tr>
)} )}
</tbody> </tbody>

View File

@@ -173,7 +173,6 @@ const DeployHistoryList = () => {
}; };
const filterList = (currentPage) => { const filterList = (currentPage) => {
const lowExceptStr = filterStr.toLowerCase(); const lowExceptStr = filterStr.toLowerCase();
const canShowList = allTopicList.filter((topic, index) => { const canShowList = allTopicList.filter((topic, index) => {
@@ -257,7 +256,7 @@ const DeployHistoryList = () => {
return; return;
} }
if(!isUpdate){ if (!isUpdate) {
const clusterResult = await remoteApi.getClusterList(); const clusterResult = await remoteApi.getClusterList();
if (clusterResult.status === 0) { if (clusterResult.status === 0) {
setAllClusterNameList(Object.keys(clusterResult.data.clusterInfo.clusterAddrTable)); setAllClusterNameList(Object.keys(clusterResult.data.clusterInfo.clusterAddrTable));
@@ -276,7 +275,7 @@ const DeployHistoryList = () => {
if (result.status === 0) { if (result.status === 0) {
messageApi.success(t.TOPIC_OPERATION_SUCCESS); messageApi.success(t.TOPIC_OPERATION_SUCCESS);
closeAddUpdateDialog(); closeAddUpdateDialog();
if(!isUpdateMode) { if (!isUpdateMode) {
await getTopicList() await getTopicList()
} }
} else { } else {

View File

@@ -17,7 +17,7 @@
const reportWebVitals = onPerfEntry => { const reportWebVitals = onPerfEntry => {
if (onPerfEntry && onPerfEntry instanceof Function) { if (onPerfEntry && onPerfEntry instanceof Function) {
import('web-vitals').then(({ getCLS, getFID, getFCP, getLCP, getTTFB }) => { import('web-vitals').then(({getCLS, getFID, getFCP, getLCP, getTTFB}) => {
getCLS(onPerfEntry); getCLS(onPerfEntry);
getFID(onPerfEntry); getFID(onPerfEntry);
getFCP(onPerfEntry); getFCP(onPerfEntry);

View File

@@ -64,7 +64,7 @@ const AppRouter = () => {
useEffect(() => { useEffect(() => {
remoteApi.setRedirectHandler(() => { remoteApi.setRedirectHandler(() => {
navigate('/login', { replace: true }); navigate('/login', {replace: true});
}); });
}, [navigate]); }, [navigate]);

View File

@@ -14,10 +14,10 @@
* See the License for the specific language governing permissions and * See the License for the specific language governing permissions and
* limitations under the License. * limitations under the License.
*/ */
import { useEffect } from 'react'; import {useEffect} from 'react';
import { useSelector, useDispatch } from 'react-redux'; import {useDispatch, useSelector} from 'react-redux';
import { themes, defaultTheme } from '../../assets/styles/theme'; import {defaultTheme, themes} from '../../assets/styles/theme';
import { setTheme } from '../actions/themeActions'; import {setTheme} from '../actions/themeActions';
export const useTheme = () => { export const useTheme = () => {
// 从 Redux store 中取出 currentThemeName // 从 Redux store 中取出 currentThemeName

View File

@@ -14,7 +14,7 @@
* See the License for the specific language governing permissions and * See the License for the specific language governing permissions and
* limitations under the License. * limitations under the License.
*/ */
import { createStore,combineReducers } from 'redux'; import {combineReducers, createStore} from 'redux';
import themeReducer from './reducers/themeReducer'; import themeReducer from './reducers/themeReducer';
// 组合所有的 reducers // 组合所有的 reducers

View File

@@ -15,7 +15,7 @@
* limitations under the License. * limitations under the License.
*/ */
import { SET_THEME } from '../actions/themeActions'; import {SET_THEME} from '../actions/themeActions';
const getInitialTheme = () => { const getInitialTheme = () => {
return localStorage.getItem('appTheme') || 'default'; return localStorage.getItem('appTheme') || 'default';

View File

@@ -16,7 +16,8 @@
~ limitations under the License. ~ limitations under the License.
--> -->
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd"> <project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<parent> <parent>
<groupId>org.apache</groupId> <groupId>org.apache</groupId>
@@ -432,7 +433,7 @@
</configuration> </configuration>
<executions> <executions>
<execution> <execution>
<id>install node </id> <id>install node</id>
<goals> <goals>
<goal>install-node-and-npm</goal> <goal>install-node-and-npm</goal>
</goals> </goals>
@@ -469,7 +470,7 @@
<configuration> <configuration>
<target> <target>
<copy todir="${project.build.directory}/classes/public"> <copy todir="${project.build.directory}/classes/public">
<fileset dir="${project.basedir}/frontend-new/build" /> <fileset dir="${project.basedir}/frontend-new/build"/>
</copy> </copy>
</target> </target>
</configuration> </configuration>

View File

@@ -91,6 +91,10 @@ public class RMQConfigure {
@Getter @Getter
private Integer clientCallbackExecutorThreads = 4; private Integer clientCallbackExecutorThreads = 4;
@Setter
@Getter
private String authMode = "file";
public void setProxyAddrs(List<String> proxyAddrs) { public void setProxyAddrs(List<String> proxyAddrs) {
this.proxyAddrs = proxyAddrs; this.proxyAddrs = proxyAddrs;
if (CollectionUtils.isNotEmpty(proxyAddrs)) { if (CollectionUtils.isNotEmpty(proxyAddrs)) {
@@ -112,10 +116,12 @@ public class RMQConfigure {
logger.info("setNameSrvAddrByProperty nameSrvAddr={}", namesrvAddr); logger.info("setNameSrvAddrByProperty nameSrvAddr={}", namesrvAddr);
} }
} }
public boolean isACLEnabled() { public boolean isACLEnabled() {
return !(StringUtils.isAnyBlank(this.accessKey, this.secretKey) || return !(StringUtils.isAnyBlank(this.accessKey, this.secretKey) ||
StringUtils.isAnyEmpty(this.accessKey, this.secretKey)); StringUtils.isAnyEmpty(this.accessKey, this.secretKey));
} }
public String getRocketMqDashboardDataPath() { public String getRocketMqDashboardDataPath() {
return dataPath; return dataPath;
} }

View File

@@ -23,6 +23,7 @@ import org.apache.rocketmq.dashboard.model.request.UserUpdateRequest;
import org.apache.rocketmq.dashboard.service.impl.AclServiceImpl; import org.apache.rocketmq.dashboard.service.impl.AclServiceImpl;
import org.apache.rocketmq.remoting.protocol.body.UserInfo; import org.apache.rocketmq.remoting.protocol.body.UserInfo;
import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.DeleteMapping; import org.springframework.web.bind.annotation.DeleteMapping;
import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PostMapping; import org.springframework.web.bind.annotation.PostMapping;
@@ -31,24 +32,23 @@ import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod; import org.springframework.web.bind.annotation.RequestMethod;
import org.springframework.web.bind.annotation.RequestParam; import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.ResponseBody; import org.springframework.web.bind.annotation.ResponseBody;
import org.springframework.web.bind.annotation.RestController;
import java.util.List; import java.util.List;
@RestController @Controller
@RequestMapping("/acl") @RequestMapping("/acl")
public class AclController { public class AclController {
@Autowired @Autowired
private AclServiceImpl aclService; private AclServiceImpl aclService;
@GetMapping("/listUsers") @GetMapping("/users.query")
@ResponseBody @ResponseBody
public List<UserInfo> listUsers(@RequestParam(required = false) String brokerAddress) { public List<UserInfo> listUsers(@RequestParam(required = false) String brokerAddress) {
return aclService.listUsers(brokerAddress); return aclService.listUsers(brokerAddress);
} }
@GetMapping("/listAcls") @GetMapping("/acls.query")
@ResponseBody @ResponseBody
public Object listAcls( public Object listAcls(
@RequestParam(required = false) String brokerAddress, @RequestParam(required = false) String brokerAddress,
@@ -56,34 +56,34 @@ public class AclController {
return aclService.listAcls(brokerAddress, searchParam); return aclService.listAcls(brokerAddress, searchParam);
} }
@PostMapping("/createAcl") @PostMapping("/createAcl.do")
@ResponseBody @ResponseBody
public Object createAcl(@RequestBody PolicyRequest request) { public Object createAcl(@RequestBody PolicyRequest request) {
aclService.createAcl(request); aclService.createAcl(request);
return true; return true;
} }
@DeleteMapping("/deleteUser") @DeleteMapping("/deleteUser.do")
@ResponseBody @ResponseBody
public Object deleteUser(@RequestParam(required = false) String brokerAddress, @RequestParam String username) { public Object deleteUser(@RequestParam(required = false) String brokerAddress, @RequestParam String username) {
aclService.deleteUser(brokerAddress, username); aclService.deleteUser(brokerAddress, username);
return true; return true;
} }
@RequestMapping(value = "/updateUser", method = RequestMethod.POST, produces = "application/json;charset=UTF-8") @RequestMapping(value = "/updateUser.do", method = RequestMethod.POST, produces = "application/json;charset=UTF-8")
@ResponseBody @ResponseBody
public Object updateUser(@RequestBody UserUpdateRequest request) { public Object updateUser(@RequestBody UserUpdateRequest request) {
aclService.updateUser(request.getBrokerAddress(), request.getUserInfo()); aclService.updateUser(request.getBrokerAddress(), request.getUserInfo());
return true; return true;
} }
@PostMapping("/createUser") @PostMapping("/createUser.do")
public Object createUser(@RequestBody UserCreateRequest request) { public Object createUser(@RequestBody UserCreateRequest request) {
aclService.createUser(request.getBrokerAddress(), request.getUserInfo()); aclService.createUser(request.getBrokerAddress(), request.getUserInfo());
return true; return true;
} }
@DeleteMapping("/deleteAcl") @DeleteMapping("/deleteAcl.do")
public Object deleteAcl( public Object deleteAcl(
@RequestParam(required = false) String brokerAddress, @RequestParam(required = false) String brokerAddress,
@RequestParam String subject, @RequestParam String subject,
@@ -92,7 +92,7 @@ public class AclController {
return true; return true;
} }
@RequestMapping(value = "/updateAcl", method = RequestMethod.POST, produces = "application/json;charset=UTF-8") @RequestMapping(value = "/updateAcl.do", method = RequestMethod.POST, produces = "application/json;charset=UTF-8")
@ResponseBody @ResponseBody
public Object updateAcl(@RequestBody PolicyRequest request) { public Object updateAcl(@RequestBody PolicyRequest request) {
aclService.updateAcl(request); aclService.updateAcl(request);

View File

@@ -47,7 +47,7 @@ public class DashboardController {
if (Strings.isNullOrEmpty(topicName)) { if (Strings.isNullOrEmpty(topicName)) {
return dashboardService.queryTopicData(date); return dashboardService.queryTopicData(date);
} }
return dashboardService.queryTopicData(date,topicName); return dashboardService.queryTopicData(date, topicName);
} }
@RequestMapping(value = "/topicCurrent.query", method = RequestMethod.GET) @RequestMapping(value = "/topicCurrent.query", method = RequestMethod.GET)

View File

@@ -31,6 +31,7 @@ import org.springframework.web.bind.annotation.ResponseBody;
public class ProxyController { public class ProxyController {
@Resource @Resource
private ProxyService proxyService; private ProxyService proxyService;
@RequestMapping(value = "/homePage.query", method = RequestMethod.GET) @RequestMapping(value = "/homePage.query", method = RequestMethod.GET)
@ResponseBody @ResponseBody
public Object homePage() { public Object homePage() {

View File

@@ -72,7 +72,8 @@ public class TestController {
new Thread(new Runnable() { new Thread(new Runnable() {
@Override public void run() { @Override
public void run() {
int i = 0; int i = 0;
while (true) { while (true) {
@@ -85,13 +86,11 @@ public class TestController {
Thread.sleep(1000L); Thread.sleep(1000L);
SendResult sendResult = producer.send(msg); SendResult sendResult = producer.send(msg);
logger.info("sendMessage={}", JsonUtil.obj2String(sendResult)); logger.info("sendMessage={}", JsonUtil.obj2String(sendResult));
} } catch (Exception e) {
catch (Exception e) {
e.printStackTrace(); e.printStackTrace();
try { try {
Thread.sleep(1000); Thread.sleep(1000);
} } catch (Exception ignore) {
catch (Exception ignore) {
} }
} }
} }

View File

@@ -75,7 +75,7 @@ public class TopicController {
} }
@RequestMapping(value = "/createOrUpdate.do", method = { RequestMethod.POST}) @RequestMapping(value = "/createOrUpdate.do", method = {RequestMethod.POST})
@ResponseBody @ResponseBody
public Object topicCreateOrUpdateRequest(@RequestBody TopicConfigInfo topicCreateOrUpdateRequest) { public Object topicCreateOrUpdateRequest(@RequestBody TopicConfigInfo topicCreateOrUpdateRequest) {
Preconditions.checkArgument(CollectionUtils.isNotEmpty(topicCreateOrUpdateRequest.getBrokerNameList()) || CollectionUtils.isNotEmpty(topicCreateOrUpdateRequest.getClusterNameList()), Preconditions.checkArgument(CollectionUtils.isNotEmpty(topicCreateOrUpdateRequest.getBrokerNameList()) || CollectionUtils.isNotEmpty(topicCreateOrUpdateRequest.getClusterNameList()),

View File

@@ -29,7 +29,6 @@ import jakarta.servlet.http.HttpServletResponse;
import java.io.IOException; import java.io.IOException;
@WebFilter(urlPatterns = "/*", filterName = "httpBasicAuthorizedFilter") @WebFilter(urlPatterns = "/*", filterName = "httpBasicAuthorizedFilter")
public class HttpBasicAuthorizedFilter implements Filter { public class HttpBasicAuthorizedFilter implements Filter {

View File

@@ -25,7 +25,9 @@ import java.util.Map;
public class MessageView { public class MessageView {
/** from MessageExt **/ /**
* from MessageExt
**/
private int queueId; private int queueId;
private int storeSize; private int storeSize;
private long queueOffset; private long queueOffset;
@@ -41,13 +43,17 @@ public class MessageView {
private long preparedTransactionOffset; private long preparedTransactionOffset;
/**from MessageExt**/ /**from MessageExt**/
/** from Message **/ /**
* from Message
**/
private String topic; private String topic;
private int flag; private int flag;
private Map<String, String> properties; private Map<String, String> properties;
private String messageBody; // body private String messageBody; // body
/** from Message **/ /**
* from Message
**/
public static MessageView fromMessageExt(MessageExt messageExt) { public static MessageView fromMessageExt(MessageExt messageExt) {
MessageView messageView = new MessageView(); MessageView messageView = new MessageView();

View File

@@ -19,8 +19,8 @@ package org.apache.rocketmq.dashboard.model;
import org.hibernate.validator.constraints.Range; import org.hibernate.validator.constraints.Range;
public class User { public class User {
public static final int ORDINARY = 0; public static final int SUPER = 0;
public static final int ADMIN = 1; public static final int NORMAL = 1;
private long id; private long id;
private String name; private String name;

View File

@@ -15,6 +15,7 @@
* limitations under the License. * limitations under the License.
*/ */
package org.apache.rocketmq.dashboard.model.request; package org.apache.rocketmq.dashboard.model.request;
import com.google.common.base.Objects; import com.google.common.base.Objects;
import java.util.List; import java.util.List;
@@ -24,7 +25,9 @@ public class TopicConfigInfo {
private List<String> clusterNameList; private List<String> clusterNameList;
private List<String> brokerNameList; private List<String> brokerNameList;
/** topicConfig */ /**
* topicConfig
*/
private String topicName; private String topicName;
private int writeQueueNums; private int writeQueueNums;
private int readQueueNums; private int readQueueNums;
@@ -32,6 +35,7 @@ public class TopicConfigInfo {
private boolean order; private boolean order;
private String messageType; private String messageType;
public List<String> getClusterNameList() { public List<String> getClusterNameList() {
return clusterNameList; return clusterNameList;
} }
@@ -40,8 +44,9 @@ public class TopicConfigInfo {
this.clusterNameList = clusterNameList; this.clusterNameList = clusterNameList;
} }
/** topicConfig */ /**
* topicConfig
*/
public List<String> getBrokerNameList() { public List<String> getBrokerNameList() {
@@ -102,8 +107,6 @@ public class TopicConfigInfo {
} }
@Override @Override
public boolean equals(Object o) { public boolean equals(Object o) {
if (this == o) if (this == o)
@@ -121,7 +124,7 @@ public class TopicConfigInfo {
@Override @Override
public int hashCode() { public int hashCode() {
return Objects.hashCode(topicName, writeQueueNums, readQueueNums, perm, order,messageType); return Objects.hashCode(topicName, writeQueueNums, readQueueNums, perm, order, messageType);
} }
} }

View File

@@ -17,9 +17,11 @@
package org.apache.rocketmq.dashboard.model.request; package org.apache.rocketmq.dashboard.model.request;
import lombok.AllArgsConstructor;
import lombok.Data; import lombok.Data;
@Data @Data
@AllArgsConstructor
public class UserInfoParam { public class UserInfoParam {
private String username; private String username;
private String password; private String password;

View File

@@ -29,6 +29,7 @@ import java.util.Set;
public abstract class AbstractCommonService { public abstract class AbstractCommonService {
@Resource @Resource
protected MQAdminExt mqAdminExt; protected MQAdminExt mqAdminExt;
protected final Set<String> changeToBrokerNameSet(Map<String, Set<String>> clusterAddrTable, protected final Set<String> changeToBrokerNameSet(Map<String, Set<String>> clusterAddrTable,
List<String> clusterNameList, List<String> brokerNameList) { List<String> clusterNameList, List<String> brokerNameList) {
Set<String> finalBrokerNameList = Sets.newHashSet(); Set<String> finalBrokerNameList = Sets.newHashSet();
@@ -37,8 +38,7 @@ public abstract class AbstractCommonService {
for (String clusterName : clusterNameList) { for (String clusterName : clusterNameList) {
finalBrokerNameList.addAll(clusterAddrTable.get(clusterName)); finalBrokerNameList.addAll(clusterAddrTable.get(clusterName));
} }
} } catch (Exception e) {
catch (Exception e) {
Throwables.throwIfUnchecked(e); Throwables.throwIfUnchecked(e);
throw new RuntimeException(e); throw new RuntimeException(e);
} }

View File

@@ -31,7 +31,7 @@ import java.util.Map;
import java.util.Set; import java.util.Set;
public interface ConsumerService { public interface ConsumerService {
List<GroupConsumeInfo> queryGroupList(boolean skipSysGroup,String address); List<GroupConsumeInfo> queryGroupList(boolean skipSysGroup, String address);
GroupConsumeInfo queryGroup(String consumerGroup, String address); GroupConsumeInfo queryGroup(String consumerGroup, String address);

View File

@@ -39,8 +39,7 @@ public interface MessageService {
/** /**
* @param topic * @param topic
* @param begin * @param begin
* @param end * @param end org.apache.rocketmq.tools.command.message.PrintMessageSubCommand
* org.apache.rocketmq.tools.command.message.PrintMessageSubCommand
*/ */
List<MessageView> queryMessageByTopic(final String topic, final long begin, List<MessageView> queryMessageByTopic(final String topic, final long begin,
final long end); final long end);
@@ -54,6 +53,4 @@ public interface MessageService {
MessagePage queryMessageByPage(MessageQuery query); MessagePage queryMessageByPage(MessageQuery query);
} }

View File

@@ -27,7 +27,7 @@ public interface OpsService {
String getNameSvrList(); String getNameSvrList();
Map<CheckerType,Object> rocketMqStatusCheck(); Map<CheckerType, Object> rocketMqStatusCheck();
boolean updateIsVIPChannel(String useVIPChannel); boolean updateIsVIPChannel(String useVIPChannel);

View File

@@ -94,7 +94,8 @@ public class MQAdminExtImpl implements MQAdminExt {
private Logger logger = LoggerFactory.getLogger(MQAdminExtImpl.class); private Logger logger = LoggerFactory.getLogger(MQAdminExtImpl.class);
public MQAdminExtImpl() {} public MQAdminExtImpl() {
}
@Override @Override
@@ -134,8 +135,7 @@ public class MQAdminExtImpl implements MQAdminExt {
RemotingCommand response = null; RemotingCommand response = null;
try { try {
response = remotingClient.invokeSync(addr, request, 8000); response = remotingClient.invokeSync(addr, request, 8000);
} } catch (Exception err) {
catch (Exception err) {
Throwables.throwIfUnchecked(err); Throwables.throwIfUnchecked(err);
throw new RuntimeException(err); throw new RuntimeException(err);
} }
@@ -566,17 +566,20 @@ public class MQAdminExtImpl implements MQAdminExt {
} }
// 4.0.0 added // 4.0.0 added
@Override public void updateNameServerConfig(Properties properties, @Override
public void updateNameServerConfig(Properties properties,
List<String> list) throws InterruptedException, RemotingConnectException, UnsupportedEncodingException, RemotingSendRequestException, RemotingTimeoutException, MQClientException, MQBrokerException { List<String> list) throws InterruptedException, RemotingConnectException, UnsupportedEncodingException, RemotingSendRequestException, RemotingTimeoutException, MQClientException, MQBrokerException {
} }
@Override public Map<String, Properties> getNameServerConfig( @Override
public Map<String, Properties> getNameServerConfig(
List<String> list) throws InterruptedException, RemotingTimeoutException, RemotingSendRequestException, RemotingConnectException, MQClientException, UnsupportedEncodingException { List<String> list) throws InterruptedException, RemotingTimeoutException, RemotingSendRequestException, RemotingConnectException, MQClientException, UnsupportedEncodingException {
return null; return null;
} }
@Override public QueryConsumeQueueResponseBody queryConsumeQueue(String brokerAddr, String topic, @Override
public QueryConsumeQueueResponseBody queryConsumeQueue(String brokerAddr, String topic,
int queueId, long index, int count, int queueId, long index, int count,
String consumerGroup) throws InterruptedException, RemotingTimeoutException, RemotingSendRequestException, RemotingConnectException, MQClientException { String consumerGroup) throws InterruptedException, RemotingTimeoutException, RemotingSendRequestException, RemotingConnectException, MQClientException {
return null; return null;
@@ -588,7 +591,8 @@ public class MQAdminExtImpl implements MQAdminExt {
} }
@Override public boolean resumeCheckHalfMessage(String topic, @Override
public boolean resumeCheckHalfMessage(String topic,
String msgId) throws RemotingException, MQClientException, InterruptedException, MQBrokerException { String msgId) throws RemotingException, MQClientException, InterruptedException, MQBrokerException {
return false; return false;
} }

View File

@@ -58,7 +58,7 @@ public abstract class AbstractFileStore {
} }
} }
abstract void load(InputStream inputStream); protected abstract void load(InputStream inputStream);
private void load() { private void load() {
load(null); load(null);
@@ -66,7 +66,7 @@ public abstract class AbstractFileStore {
private boolean watch() { private boolean watch() {
try { try {
FileWatchService fileWatchService = new FileWatchService(new String[] {filePath}, new FileWatchService.Listener() { FileWatchService fileWatchService = new FileWatchService(new String[]{filePath}, new FileWatchService.Listener() {
@Override @Override
public void onChanged(String path) { public void onChanged(String path) {
log.info("The file changed, reload the context"); log.info("The file changed, reload the context");

View File

@@ -212,7 +212,6 @@ public class ConsumerServiceImpl extends AbstractCommonService implements Consum
} else { } else {
consumeInfo.setSubGroupType(subscriptionGroupTable.get(consumerGroup).isConsumeMessageOrderly() ? "FIFO" : "NORMAL"); consumeInfo.setSubGroupType(subscriptionGroupTable.get(consumerGroup).isConsumeMessageOrderly() ? "FIFO" : "NORMAL");
} }
consumeInfo.setGroup(consumerGroup);
consumeInfo.setUpdateTime(new Date()); consumeInfo.setUpdateTime(new Date());
groupConsumeInfoList.add(consumeInfo); groupConsumeInfoList.add(consumeInfo);
} catch (Exception e) { } catch (Exception e) {
@@ -270,6 +269,7 @@ public class ConsumerServiceImpl extends AbstractCommonService implements Consum
logger.warn("examineConsumeStats or examineConsumerConnectionInfo exception, " logger.warn("examineConsumeStats or examineConsumerConnectionInfo exception, "
+ consumerGroup, e); + consumerGroup, e);
} }
groupConsumeInfo.setGroup(consumerGroup);
return groupConsumeInfo; return groupConsumeInfo;
} }

View File

@@ -96,6 +96,7 @@ public class DashboardCollectServiceImpl implements DashboardCollectService {
public LoadingCache<String, List<String>> getBrokerMap() { public LoadingCache<String, List<String>> getBrokerMap() {
return brokerMap; return brokerMap;
} }
@Override @Override
public LoadingCache<String, List<String>> getTopicMap() { public LoadingCache<String, List<String>> getTopicMap() {
return topicMap; return topicMap;
@@ -106,8 +107,7 @@ public class DashboardCollectServiceImpl implements DashboardCollectService {
List<String> strings; List<String> strings;
try { try {
strings = Files.readLines(file, Charsets.UTF_8); strings = Files.readLines(file, Charsets.UTF_8);
} } catch (IOException e) {
catch (IOException e) {
Throwables.throwIfUnchecked(e); Throwables.throwIfUnchecked(e);
throw new RuntimeException(e); throw new RuntimeException(e);
} }

View File

@@ -34,6 +34,7 @@ public class DashboardServiceImpl implements DashboardService {
@Resource @Resource
private DashboardCollectService dashboardCollectService; private DashboardCollectService dashboardCollectService;
/** /**
* @param date format yyyy-MM-dd * @param date format yyyy-MM-dd
*/ */

View File

@@ -17,13 +17,10 @@
package org.apache.rocketmq.dashboard.service.impl; package org.apache.rocketmq.dashboard.service.impl;
import jakarta.annotation.Resource;
import jakarta.servlet.http.HttpServletRequest; import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse; import jakarta.servlet.http.HttpServletResponse;
import org.apache.rocketmq.dashboard.config.RMQConfigure;
import org.apache.rocketmq.dashboard.service.LoginService; import org.apache.rocketmq.dashboard.service.LoginService;
import org.apache.rocketmq.dashboard.service.UserService; import org.apache.rocketmq.dashboard.service.strategy.UserContext;
import org.apache.rocketmq.dashboard.service.provider.UserInfoProvider;
import org.apache.rocketmq.dashboard.util.UserInfoContext; import org.apache.rocketmq.dashboard.util.UserInfoContext;
import org.apache.rocketmq.dashboard.util.WebUtil; import org.apache.rocketmq.dashboard.util.WebUtil;
import org.apache.rocketmq.remoting.protocol.body.UserInfo; import org.apache.rocketmq.remoting.protocol.body.UserInfo;
@@ -33,34 +30,29 @@ import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service; import org.springframework.stereotype.Service;
import java.io.IOException; import java.io.IOException;
import java.io.UnsupportedEncodingException;
import java.net.URLEncoder; import java.net.URLEncoder;
import java.nio.charset.StandardCharsets;
@Service @Service
public class LoginServiceImpl implements LoginService { public class LoginServiceImpl implements LoginService {
private final Logger logger = LoggerFactory.getLogger(this.getClass()); private final Logger logger = LoggerFactory.getLogger(this.getClass());
@Resource
private RMQConfigure rmqConfigure;
@Autowired @Autowired
private UserService userService; private UserContext userContext;
@Autowired
private UserInfoProvider userInfoProvider;
@Override @Override
public boolean login(HttpServletRequest request, HttpServletResponse response) { public boolean login(HttpServletRequest request, HttpServletResponse response) {
String username = (String) WebUtil.getValueFromSession(request, WebUtil.USER_NAME); String username = (String) WebUtil.getValueFromSession(request, WebUtil.USER_NAME);
if (username != null) { if (username != null) {
UserInfo userInfo = userInfoProvider.getUserInfoByUsername(username); UserInfo userInfo = userContext.queryByUsername(username);
if (userInfo == null) { if (userInfo == null) {
auth(request, response); auth(request, response);
return false; return false;
} }
UserInfoContext.set(WebUtil.USER_NAME, userInfo); UserInfoContext.set(WebUtil.USER_NAME, userInfo);
return true; return true;
} }
auth(request, response); auth(request, response);
return false; return false;
@@ -69,11 +61,7 @@ public class LoginServiceImpl implements LoginService {
protected void auth(HttpServletRequest request, HttpServletResponse response) { protected void auth(HttpServletRequest request, HttpServletResponse response) {
try { try {
String url = WebUtil.getUrl(request); String url = WebUtil.getUrl(request);
try { url = URLEncoder.encode(url, StandardCharsets.UTF_8);
url = URLEncoder.encode(url, "UTF-8");
} catch (UnsupportedEncodingException e) {
logger.error("url encode:{}", url, e);
}
logger.debug("redirect url : {}", url); logger.debug("redirect url : {}", url);
WebUtil.redirect(response, request, "/#/login?redirect=" + url); WebUtil.redirect(response, request, "/#/login?redirect=" + url);
} catch (IOException e) { } catch (IOException e) {

View File

@@ -33,7 +33,6 @@ import org.apache.rocketmq.client.consumer.DefaultMQPullConsumer;
import org.apache.rocketmq.client.consumer.PullResult; import org.apache.rocketmq.client.consumer.PullResult;
import org.apache.rocketmq.client.consumer.PullStatus; import org.apache.rocketmq.client.consumer.PullStatus;
import org.apache.rocketmq.client.exception.MQClientException; import org.apache.rocketmq.client.exception.MQClientException;
import org.apache.rocketmq.common.MixAll;
import org.apache.rocketmq.common.Pair; import org.apache.rocketmq.common.Pair;
import org.apache.rocketmq.common.message.MessageClientIDSetter; import org.apache.rocketmq.common.message.MessageClientIDSetter;
import org.apache.rocketmq.common.message.MessageExt; import org.apache.rocketmq.common.message.MessageExt;
@@ -74,6 +73,9 @@ import java.util.stream.Collectors;
@Service @Service
public class MessageServiceImpl implements MessageService { public class MessageServiceImpl implements MessageService {
@Resource
private AutoCloseConsumerWrapper autoCloseConsumerWrapper;
private Logger logger = LoggerFactory.getLogger(MessageServiceImpl.class); private Logger logger = LoggerFactory.getLogger(MessageServiceImpl.class);
private static final Cache<String, List<QueueOffsetInfo>> CACHE = CacheBuilder.newBuilder() private static final Cache<String, List<QueueOffsetInfo>> CACHE = CacheBuilder.newBuilder()
@@ -128,8 +130,8 @@ public class MessageServiceImpl implements MessageService {
if (isEnableAcl) { if (isEnableAcl) {
rpcHook = new AclClientRPCHook(new SessionCredentials(configure.getAccessKey(), configure.getSecretKey())); rpcHook = new AclClientRPCHook(new SessionCredentials(configure.getAccessKey(), configure.getSecretKey()));
} }
AutoCloseConsumerWrapper consumerWrapper = new AutoCloseConsumerWrapper();
DefaultMQPullConsumer consumer = consumerWrapper.getConsumer(rpcHook, configure.isUseTLS()); DefaultMQPullConsumer consumer = autoCloseConsumerWrapper.getConsumer(rpcHook, configure.isUseTLS());
List<MessageView> messageViewList = Lists.newArrayList(); List<MessageView> messageViewList = Lists.newArrayList();
try { try {
String subExpression = "*"; String subExpression = "*";
@@ -262,8 +264,8 @@ public class MessageServiceImpl implements MessageService {
if (isEnableAcl) { if (isEnableAcl) {
rpcHook = new AclClientRPCHook(new SessionCredentials(configure.getAccessKey(), configure.getSecretKey())); rpcHook = new AclClientRPCHook(new SessionCredentials(configure.getAccessKey(), configure.getSecretKey()));
} }
AutoCloseConsumerWrapper consumerWrapper = new AutoCloseConsumerWrapper();
DefaultMQPullConsumer consumer = consumerWrapper.getConsumer(rpcHook, configure.isUseTLS()); DefaultMQPullConsumer consumer = autoCloseConsumerWrapper.getConsumer(rpcHook, configure.isUseTLS());
long total = 0; long total = 0;
List<QueueOffsetInfo> queueOffsetInfos = new ArrayList<>(); List<QueueOffsetInfo> queueOffsetInfos = new ArrayList<>();
@@ -402,8 +404,8 @@ public class MessageServiceImpl implements MessageService {
if (isEnableAcl) { if (isEnableAcl) {
rpcHook = new AclClientRPCHook(new SessionCredentials(configure.getAccessKey(), configure.getSecretKey())); rpcHook = new AclClientRPCHook(new SessionCredentials(configure.getAccessKey(), configure.getSecretKey()));
} }
AutoCloseConsumerWrapper consumerWrapper = new AutoCloseConsumerWrapper();
DefaultMQPullConsumer consumer = consumerWrapper.getConsumer(rpcHook, configure.isUseTLS()); DefaultMQPullConsumer consumer = autoCloseConsumerWrapper.getConsumer(rpcHook, configure.isUseTLS());
List<MessageView> messageViews = new ArrayList<>(); List<MessageView> messageViews = new ArrayList<>();
long offset = query.getPageNum() * query.getPageSize(); long offset = query.getPageNum() * query.getPageSize();
@@ -541,9 +543,9 @@ public class MessageServiceImpl implements MessageService {
} }
} }
public DefaultMQPullConsumer buildDefaultMQPullConsumer(RPCHook rpcHook, boolean useTLS) { // public DefaultMQPullConsumer buildDefaultMQPullConsumer(RPCHook rpcHook, boolean useTLS) {
DefaultMQPullConsumer consumer = new DefaultMQPullConsumer(MixAll.TOOLS_CONSUMER_GROUP, rpcHook); // DefaultMQPullConsumer consumer = new DefaultMQPullConsumer(MixAll.TOOLS_CONSUMER_GROUP, rpcHook);
consumer.setUseTLS(useTLS); // consumer.setUseTLS(useTLS);
return consumer; // return consumer;
} // }
} }

View File

@@ -81,8 +81,7 @@ public class MonitorServiceImpl implements MonitorService {
private void writeDataJsonToFile(String path, String dataStr) { private void writeDataJsonToFile(String path, String dataStr) {
try { try {
MixAll.string2File(dataStr, path); MixAll.string2File(dataStr, path);
} } catch (Exception e) {
catch (Exception e) {
Throwables.throwIfUnchecked(e); Throwables.throwIfUnchecked(e);
throw new RuntimeException(e); throw new RuntimeException(e);
} }

View File

@@ -33,8 +33,7 @@ public class ProducerServiceImpl implements ProducerService {
public ProducerConnection getProducerConnection(String producerGroup, String topic) { public ProducerConnection getProducerConnection(String producerGroup, String topic) {
try { try {
return mqAdminExt.examineProducerConnectionInfo(producerGroup, topic); return mqAdminExt.examineProducerConnectionInfo(producerGroup, topic);
} } catch (Exception e) {
catch (Exception e) {
Throwables.throwIfUnchecked(e); Throwables.throwIfUnchecked(e);
throw new RuntimeException(e); throw new RuntimeException(e);
} }

View File

@@ -127,7 +127,7 @@ public class TopicServiceImpl extends AbstractCommonService implements TopicServ
try { try {
TopicConfigSerializeWrapper topicConfigSerializeWrapper = mqAdminExt.getAllTopicConfig(brokerAddr.getBrokerAddrs().get(0L), 10000L); TopicConfigSerializeWrapper topicConfigSerializeWrapper = mqAdminExt.getAllTopicConfig(brokerAddr.getBrokerAddrs().get(0L), 10000L);
for (TopicConfig topicConfig : topicConfigSerializeWrapper.getTopicConfigTable().values()) { for (TopicConfig topicConfig : topicConfigSerializeWrapper.getTopicConfigTable().values()) {
TopicTypeMeta topicType = classifyTopicType(topicConfig.getTopicName(), topicConfigSerializeWrapper.getTopicConfigTable().get(topicConfig.getTopicName()).getAttributes(),sysTopics.getTopicList()); TopicTypeMeta topicType = classifyTopicType(topicConfig.getTopicName(), topicConfigSerializeWrapper.getTopicConfigTable().get(topicConfig.getTopicName()).getAttributes(), sysTopics.getTopicList());
if (names.contains(topicType.getTopicName())) { if (names.contains(topicType.getTopicName())) {
continue; continue;
} }
@@ -149,7 +149,7 @@ public class TopicServiceImpl extends AbstractCommonService implements TopicServ
return new TopicTypeList(names, messageTypes); return new TopicTypeList(names, messageTypes);
} }
private TopicTypeMeta classifyTopicType(String topicName, Map<String,String> attributes, Set<String> sysTopics) { private TopicTypeMeta classifyTopicType(String topicName, Map<String, String> attributes, Set<String> sysTopics) {
TopicTypeMeta topicType = new TopicTypeMeta(); TopicTypeMeta topicType = new TopicTypeMeta();
topicType.setTopicName(topicName); topicType.setTopicName(topicName);

View File

@@ -17,29 +17,26 @@
package org.apache.rocketmq.dashboard.service.impl; package org.apache.rocketmq.dashboard.service.impl;
import jakarta.annotation.Resource;
import org.apache.rocketmq.auth.authentication.enums.UserType; import org.apache.rocketmq.auth.authentication.enums.UserType;
import org.apache.rocketmq.dashboard.admin.UserMQAdminPoolManager; import org.apache.rocketmq.dashboard.admin.UserMQAdminPoolManager;
import org.apache.rocketmq.dashboard.config.RMQConfigure;
import org.apache.rocketmq.dashboard.model.User; import org.apache.rocketmq.dashboard.model.User;
import org.apache.rocketmq.dashboard.service.UserService; import org.apache.rocketmq.dashboard.service.UserService;
import org.apache.rocketmq.dashboard.service.provider.UserInfoProvider; import org.apache.rocketmq.dashboard.service.strategy.UserContext;
import org.apache.rocketmq.remoting.protocol.body.UserInfo; import org.apache.rocketmq.remoting.protocol.body.UserInfo;
import org.apache.rocketmq.tools.admin.MQAdminExt; import org.apache.rocketmq.tools.admin.MQAdminExt;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.slf4j.Logger; import org.slf4j.Logger;
import org.slf4j.LoggerFactory; import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
@Service @Service
public class UserServiceImpl implements UserService { public class UserServiceImpl implements UserService {
private static final Logger log = LoggerFactory.getLogger(UserServiceImpl.class); private static final Logger log = LoggerFactory.getLogger(UserServiceImpl.class);
@Resource
private RMQConfigure configure;
@Autowired @Autowired
private UserInfoProvider userInfoProvider; private UserContext userContext;
@Autowired @Autowired
private UserMQAdminPoolManager userMQAdminPoolManager; private UserMQAdminPoolManager userMQAdminPoolManager;
@@ -47,7 +44,7 @@ public class UserServiceImpl implements UserService {
@Override @Override
public User queryByName(String name) { public User queryByName(String name) {
UserInfo userInfo = userInfoProvider.getUserInfoByUsername(name); UserInfo userInfo = userContext.queryByUsername(name);
if (userInfo == null) { if (userInfo == null) {
return null; return null;
} }

View File

@@ -0,0 +1,67 @@
/*
* 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.
*/
package org.apache.rocketmq.dashboard.service.strategy;
import lombok.AllArgsConstructor;
import org.apache.rocketmq.dashboard.service.ClusterInfoService;
import org.apache.rocketmq.remoting.protocol.body.ClusterInfo;
import org.apache.rocketmq.remoting.protocol.body.UserInfo;
import org.apache.rocketmq.remoting.protocol.route.BrokerData;
import org.apache.rocketmq.tools.admin.MQAdminExt;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.stereotype.Service;
@Service
@AllArgsConstructor
public class AclUserStrategy implements UserStrategy {
private static final Logger log = LoggerFactory.getLogger(AclUserStrategy.class);
private final MQAdminExt mqAdminExt;
private final ClusterInfoService clusterInfoService;
@Override
public UserInfo getUserInfoByUsername(String username) {
ClusterInfo clusterInfo = clusterInfoService.get();
if (clusterInfo == null || clusterInfo.getBrokerAddrTable() == null || clusterInfo.getBrokerAddrTable().isEmpty()) {
log.warn("Cluster information is not available or has no broker addresses.");
return null;
}
for (BrokerData brokerLiveInfo : clusterInfo.getBrokerAddrTable().values()) {
if (brokerLiveInfo == null || brokerLiveInfo.getBrokerAddrs() == null || brokerLiveInfo.getBrokerAddrs().isEmpty()) {
continue;
}
String brokerAddr = brokerLiveInfo.getBrokerAddrs().get(0L); // Assuming 0L is the primary address
if (brokerAddr == null) {
continue;
}
try {
UserInfo userInfo = mqAdminExt.getUser(brokerAddr, username);
if (userInfo != null) {
return userInfo;
}
} catch (Exception e) {
log.warn("Failed to get user {} from broker {}. Trying next broker if available. Error: {}", username, brokerAddr, e.getMessage());
}
}
return null;
}
}

View File

@@ -0,0 +1,119 @@
/*
* 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.
*/
package org.apache.rocketmq.dashboard.service.strategy;
import jakarta.annotation.Resource;
import jakarta.validation.constraints.NotNull;
import org.apache.rocketmq.dashboard.config.RMQConfigure;
import org.apache.rocketmq.dashboard.exception.ServiceException;
import org.apache.rocketmq.dashboard.model.User;
import org.apache.rocketmq.dashboard.service.impl.AbstractFileStore;
import org.apache.rocketmq.remoting.protocol.body.UserInfo;
import org.springframework.beans.factory.InitializingBean;
import org.springframework.stereotype.Service;
import java.io.FileReader;
import java.io.InputStream;
import java.util.HashMap;
import java.util.Map;
import java.util.Properties;
import java.util.concurrent.ConcurrentHashMap;
@Service
public class FileUserStrategy implements UserStrategy, InitializingBean {
@Resource
private RMQConfigure configure;
private FileBasedUserInfoStore fileBasedUserInfoStore;
@Override
public void afterPropertiesSet() throws Exception {
if (configure.isLoginRequired()) {
fileBasedUserInfoStore = new FileBasedUserInfoStore(configure);
}
}
@Override
public UserInfo getUserInfoByUsername(String username) {
User user = fileBasedUserInfoStore.queryByUsernameAndPassword(username);
if (user != null) {
return UserInfo.of(user.getName(), user.getPassword(), user.getType() == 0 ? "normal" : "super");
}
return null;
}
public static class FileBasedUserInfoStore extends AbstractFileStore {
private static final String FILE_NAME = "users.properties";
private static Map<String, User> userMap = new ConcurrentHashMap<>();
public FileBasedUserInfoStore(RMQConfigure configure) {
super(configure, FILE_NAME);
}
@Override
public void load(InputStream inputStream) {
Properties prop = new Properties();
try {
if (inputStream == null) {
prop.load(new FileReader(filePath));
} else {
prop.load(inputStream);
}
} catch (Exception e) {
log.error("load user.properties failed", e);
throw new ServiceException(0, String.format("Failed to load loginUserInfo property file: %s", filePath));
}
Map<String, User> loadUserMap = new HashMap<>();
String[] arrs;
int role;
for (String key : prop.stringPropertyNames()) {
String v = prop.getProperty(key);
if (v == null)
continue;
arrs = v.split(",", 2);
if (arrs.length == 0) {
continue;
} else if (arrs.length == 1) {
role = 0;
} else {
role = Integer.parseInt(arrs[1].trim());
}
loadUserMap.put(key, new User(key, arrs[0].trim(), role));
}
userMap.clear();
userMap.putAll(loadUserMap);
}
public User queryByName(String name) {
return userMap.get(name);
}
public User queryByUsernameAndPassword(@NotNull String username) {
User user = queryByName(username);
if (user != null) {
return user.cloneOne();
}
return null;
}
}
}

View File

@@ -0,0 +1,54 @@
/*
* 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.
*/
package org.apache.rocketmq.dashboard.service.strategy;
import jakarta.annotation.PostConstruct;
import org.apache.rocketmq.remoting.protocol.body.UserInfo;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Component;
import java.util.Map;
@Component
public class UserContext {
private UserStrategy userStrategy;
@Autowired
private Map<String, UserStrategy> userStrategies;
@Value("${rocketmq.config.authMode}")
private String authMode;
@PostConstruct
public void init() {
switch (authMode.toLowerCase()) {
case "acl":
this.userStrategy = userStrategies.get("aclUserStrategy");
break;
case "file":
default:
this.userStrategy = userStrategies.get("fileUserStrategy");
break;
}
}
public UserInfo queryByUsername(String username) {
return userStrategy.getUserInfoByUsername(username);
}
}

View File

@@ -15,11 +15,10 @@
* limitations under the License. * limitations under the License.
*/ */
package org.apache.rocketmq.dashboard.admin; package org.apache.rocketmq.dashboard.service.strategy;
import org.apache.rocketmq.tools.admin.MQAdminExt; import org.apache.rocketmq.remoting.protocol.body.UserInfo;
@FunctionalInterface public interface UserStrategy {
public interface MQAdminExtCallback<T> { UserInfo getUserInfoByUsername(String username);
T doInMQAdminExt(MQAdminExt mqAdminExt) throws Exception;
} }

View File

@@ -23,6 +23,7 @@ import org.apache.rocketmq.common.MixAll;
import org.apache.rocketmq.remoting.RPCHook; import org.apache.rocketmq.remoting.RPCHook;
import org.slf4j.Logger; import org.slf4j.Logger;
import org.slf4j.LoggerFactory; import org.slf4j.LoggerFactory;
import org.springframework.stereotype.Component;
import java.time.Duration; import java.time.Duration;
import java.time.Instant; import java.time.Instant;
@@ -32,9 +33,10 @@ import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicBoolean; import java.util.concurrent.atomic.AtomicBoolean;
import java.util.concurrent.atomic.AtomicReference; import java.util.concurrent.atomic.AtomicReference;
@Component
public class AutoCloseConsumerWrapper { public class AutoCloseConsumerWrapper {
private final Logger logger = LoggerFactory.getLogger(GlobalRestfulResponseBodyAdvice.class); private final Logger logger = LoggerFactory.getLogger(AutoCloseConsumerWrapper.class);
private static final AtomicReference<DefaultMQPullConsumer> CONSUMER_REF = new AtomicReference<>(); private static final AtomicReference<DefaultMQPullConsumer> CONSUMER_REF = new AtomicReference<>();
private final AtomicBoolean isTaskScheduled = new AtomicBoolean(false); private final AtomicBoolean isTaskScheduled = new AtomicBoolean(false);
@@ -77,7 +79,10 @@ public class AutoCloseConsumerWrapper {
protected DefaultMQPullConsumer createNewConsumer(RPCHook rpcHook, Boolean useTLS) { protected DefaultMQPullConsumer createNewConsumer(RPCHook rpcHook, Boolean useTLS) {
return new DefaultMQPullConsumer(MixAll.TOOLS_CONSUMER_GROUP, rpcHook) { return new DefaultMQPullConsumer(MixAll.TOOLS_CONSUMER_GROUP, rpcHook) {
{ setUseTLS(useTLS); } }; {
setUseTLS(useTLS);
}
};
} }
private void startIdleCheckTask() { private void startIdleCheckTask() {

View File

@@ -37,8 +37,7 @@ public class GlobalExceptionHandler {
if (ex instanceof ServiceException) { if (ex instanceof ServiceException) {
logger.error("Occur service exception: {}", ex.getMessage()); logger.error("Occur service exception: {}", ex.getMessage());
value = new JsonResult<Object>(((ServiceException) ex).getCode(), ex.getMessage()); value = new JsonResult<Object>(((ServiceException) ex).getCode(), ex.getMessage());
} } else {
else {
logger.error("op=global_exception_handler_print_error", ex); logger.error("op=global_exception_handler_print_error", ex);
value = new JsonResult<Object>(-1, ex.getMessage() == null ? ex.toString() : ex.getMessage()); value = new JsonResult<Object>(-1, ex.getMessage() == null ? ex.toString() : ex.getMessage());
} }

View File

@@ -46,9 +46,8 @@ public class GlobalRestfulResponseBodyAdvice implements ResponseBodyAdvice<Objec
} }
JsonResult value; JsonResult value;
if (obj instanceof JsonResult) { if (obj instanceof JsonResult) {
value = (JsonResult)obj; value = (JsonResult) obj;
} } else {
else {
value = new JsonResult(obj); value = new JsonResult(obj);
} }
return value; return value;

View File

@@ -87,8 +87,7 @@ public class DashboardCollectTask {
CollectTaskRunnble collectTask = new CollectTaskRunnble(topic, mqAdminExt, dashboardCollectService); CollectTaskRunnble collectTask = new CollectTaskRunnble(topic, mqAdminExt, dashboardCollectService);
collectExecutor.submit(collectTask); collectExecutor.submit(collectTask);
} }
} } catch (Exception err) {
catch (Exception err) {
Throwables.throwIfUnchecked(err); Throwables.throwIfUnchecked(err);
throw new RuntimeException(err); throw new RuntimeException(err);
} }
@@ -100,7 +99,6 @@ public class DashboardCollectTask {
} }
@Scheduled(cron = "0 0/1 * * * ?") @Scheduled(cron = "0 0/1 * * * ?")
public void collectBroker() { public void collectBroker() {
if (!rmqConfigure.isEnableDashBoardCollect()) { if (!rmqConfigure.isEnableDashBoardCollect()) {
@@ -139,8 +137,7 @@ public class DashboardCollectTask {
dashboardCollectService.getBrokerMap().put(entry.getValue(), list); dashboardCollectService.getBrokerMap().put(entry.getValue(), list);
} }
log.debug("Broker Collected Data in memory = {}" + JsonUtil.obj2String(dashboardCollectService.getBrokerMap().asMap())); log.debug("Broker Collected Data in memory = {}" + JsonUtil.obj2String(dashboardCollectService.getBrokerMap().asMap()));
} } catch (Exception e) {
catch (Exception e) {
Throwables.throwIfUnchecked(e); Throwables.throwIfUnchecked(e);
throw new RuntimeException(e); throw new RuntimeException(e);
} }
@@ -152,12 +149,10 @@ public class DashboardCollectTask {
} }
try { try {
return mqAdminExt.fetchBrokerRuntimeStats(brokerAddr); return mqAdminExt.fetchBrokerRuntimeStats(brokerAddr);
} } catch (Exception e) {
catch (Exception e) {
try { try {
Thread.sleep(1000); Thread.sleep(1000);
} } catch (InterruptedException e1) {
catch (InterruptedException e1) {
Throwables.throwIfUnchecked(e1); Throwables.throwIfUnchecked(e1);
throw new RuntimeException(e1); throw new RuntimeException(e1);
} }
@@ -189,16 +184,14 @@ public class DashboardCollectTask {
Map<String, List<String>> topicFileMap; Map<String, List<String>> topicFileMap;
if (brokerFile.exists()) { if (brokerFile.exists()) {
brokerFileMap = dashboardCollectService.jsonDataFile2map(brokerFile); brokerFileMap = dashboardCollectService.jsonDataFile2map(brokerFile);
} } else {
else {
brokerFileMap = Maps.newHashMap(); brokerFileMap = Maps.newHashMap();
Files.createParentDirs(brokerFile); Files.createParentDirs(brokerFile);
} }
if (topicFile.exists()) { if (topicFile.exists()) {
topicFileMap = dashboardCollectService.jsonDataFile2map(topicFile); topicFileMap = dashboardCollectService.jsonDataFile2map(topicFile);
} } else {
else {
topicFileMap = Maps.newHashMap(); topicFileMap = Maps.newHashMap();
Files.createParentDirs(topicFile); Files.createParentDirs(topicFile);
} }
@@ -211,8 +204,7 @@ public class DashboardCollectTask {
log.debug("Broker Collected Data in memory = {}" + JsonUtil.obj2String(dashboardCollectService.getBrokerMap().asMap())); log.debug("Broker Collected Data in memory = {}" + JsonUtil.obj2String(dashboardCollectService.getBrokerMap().asMap()));
log.debug("Topic Collected Data in memory = {}" + JsonUtil.obj2String(dashboardCollectService.getTopicMap().asMap())); log.debug("Topic Collected Data in memory = {}" + JsonUtil.obj2String(dashboardCollectService.getTopicMap().asMap()));
} } catch (IOException e) {
catch (IOException e) {
Throwables.throwIfUnchecked(e); Throwables.throwIfUnchecked(e);
throw new RuntimeException(e); throw new RuntimeException(e);
} }
@@ -224,8 +216,7 @@ public class DashboardCollectTask {
Map<String, List<String>> resultMap = Maps.newHashMap(); Map<String, List<String>> resultMap = Maps.newHashMap();
if (fileMap.size() == 0) { if (fileMap.size() == 0) {
resultMap = newMap; resultMap = newMap;
} } else {
else {
for (Map.Entry<String, List<String>> entry : fileMap.entrySet()) { for (Map.Entry<String, List<String>> entry : fileMap.entrySet()) {
List<String> oldList = entry.getValue(); List<String> oldList = entry.getValue();
List<String> newList = newMap.get(entry.getKey()); List<String> newList = newMap.get(entry.getKey());

View File

@@ -38,7 +38,7 @@ public class MonitorTask {
@Resource @Resource
private ConsumerService consumerService; private ConsumerService consumerService;
// @Scheduled(cron = "* * * * * ?") // @Scheduled(cron = "* * * * * ?")
public void scanProblemConsumeGroup() { public void scanProblemConsumeGroup() {
for (Map.Entry<String, ConsumerMonitorConfig> configEntry : monitorService.queryConsumerMonitorConfig().entrySet()) { for (Map.Entry<String, ConsumerMonitorConfig> configEntry : monitorService.queryConsumerMonitorConfig().entrySet()) {
GroupConsumeInfo consumeInfo = consumerService.queryGroup(configEntry.getKey(), null); GroupConsumeInfo consumeInfo = consumerService.queryGroup(configEntry.getKey(), null);

View File

@@ -34,7 +34,7 @@ public class ExcelUtil {
String sheetName, Class clazz) throws Exception { String sheetName, Class clazz) throws Exception {
WriteCellStyle headWriteCellStyle = new WriteCellStyle(); WriteCellStyle headWriteCellStyle = new WriteCellStyle();
WriteFont writeFont = new WriteFont(); WriteFont writeFont = new WriteFont();
writeFont.setFontHeightInPoints((short)12); writeFont.setFontHeightInPoints((short) 12);
writeFont.setFontName("Microsoft YaHei UI"); writeFont.setFontName("Microsoft YaHei UI");
headWriteCellStyle.setWriteFont(writeFont); headWriteCellStyle.setWriteFont(writeFont);
headWriteCellStyle.setHorizontalAlignment(HorizontalAlignment.CENTER); headWriteCellStyle.setHorizontalAlignment(HorizontalAlignment.CENTER);

View File

@@ -55,8 +55,7 @@ public class JsonUtil {
public static void writeValue(Writer writer, Object obj) { public static void writeValue(Writer writer, Object obj) {
try { try {
objectMapper.writeValue(writer, obj); objectMapper.writeValue(writer, obj);
} } catch (IOException e) {
catch (IOException e) {
Throwables.propagateIfPossible(e); Throwables.propagateIfPossible(e);
} }
} }
@@ -67,9 +66,8 @@ public class JsonUtil {
} }
try { try {
return src instanceof String ? (String)src : objectMapper.writeValueAsString(src); return src instanceof String ? (String) src : objectMapper.writeValueAsString(src);
} } catch (Exception e) {
catch (Exception e) {
logger.error("Parse Object to String error src=" + src, e); logger.error("Parse Object to String error src=" + src, e);
return null; return null;
} }
@@ -81,9 +79,8 @@ public class JsonUtil {
} }
try { try {
return src instanceof byte[] ? (byte[])src : objectMapper.writeValueAsBytes(src); return src instanceof byte[] ? (byte[]) src : objectMapper.writeValueAsBytes(src);
} } catch (Exception e) {
catch (Exception e) {
logger.error("Parse Object to byte[] error", e); logger.error("Parse Object to byte[] error", e);
return null; return null;
} }
@@ -95,9 +92,8 @@ public class JsonUtil {
} }
str = escapesSpecialChar(str); str = escapesSpecialChar(str);
try { try {
return clazz.equals(String.class) ? (T)str : objectMapper.readValue(str, clazz); return clazz.equals(String.class) ? (T) str : objectMapper.readValue(str, clazz);
} } catch (Exception e) {
catch (Exception e) {
logger.error("Parse String to Object error\nString: {}\nClass<T>: {}\nError: {}", str, clazz.getName(), e); logger.error("Parse String to Object error\nString: {}\nClass<T>: {}\nError: {}", str, clazz.getName(), e);
return null; return null;
} }
@@ -108,9 +104,8 @@ public class JsonUtil {
return null; return null;
} }
try { try {
return clazz.equals(byte[].class) ? (T)bytes : objectMapper.readValue(bytes, clazz); return clazz.equals(byte[].class) ? (T) bytes : objectMapper.readValue(bytes, clazz);
} } catch (Exception e) {
catch (Exception e) {
logger.error("Parse byte[] to Object error\nbyte[]: {}\nClass<T>: {}\nError: {}", bytes, clazz.getName(), e); logger.error("Parse byte[] to Object error\nbyte[]: {}\nClass<T>: {}\nError: {}", bytes, clazz.getName(), e);
return null; return null;
} }
@@ -122,9 +117,8 @@ public class JsonUtil {
} }
str = escapesSpecialChar(str); str = escapesSpecialChar(str);
try { try {
return (T)(typeReference.getType().equals(String.class) ? str : objectMapper.readValue(str, typeReference)); return (T) (typeReference.getType().equals(String.class) ? str : objectMapper.readValue(str, typeReference));
} } catch (Exception e) {
catch (Exception e) {
logger.error("Parse String to Object error\nString: {}\nTypeReference<T>: {}\nError: {}", str, logger.error("Parse String to Object error\nString: {}\nTypeReference<T>: {}\nError: {}", str,
typeReference.getType(), e); typeReference.getType(), e);
return null; return null;
@@ -136,10 +130,9 @@ public class JsonUtil {
return null; return null;
} }
try { try {
return (T)(typeReference.getType().equals(byte[].class) ? bytes : objectMapper.readValue(bytes, return (T) (typeReference.getType().equals(byte[].class) ? bytes : objectMapper.readValue(bytes,
typeReference)); typeReference));
} } catch (Exception e) {
catch (Exception e) {
logger.error("Parse byte[] to Object error\nbyte[]: {}\nTypeReference<T>: {}\nError: {}", bytes, logger.error("Parse byte[] to Object error\nbyte[]: {}\nTypeReference<T>: {}\nError: {}", bytes,
typeReference.getType(), e); typeReference.getType(), e);
return null; return null;

View File

@@ -58,6 +58,12 @@ rocketmq:
ticketKey: ticket ticketKey: ticket
# must create userInfo file: ${rocketmq.config.dataPath}/users.properties if the login is required # must create userInfo file: ${rocketmq.config.dataPath}/users.properties if the login is required
loginRequired: false loginRequired: false
# Authentication mode for RocketMQ Dashboard
# Available options:
# - 'file': Use username/password stored in a file (requires 'auth.file.path')
# - 'acl': Use credentials from ACL system (requires 'acl.access.key' and 'acl.secret.key')
# Default: file
authMode: file
useTLS: false useTLS: false
proxyAddr: 127.0.0.1:8080 proxyAddr: 127.0.0.1:8080
proxyAddrs: proxyAddrs:

View File

@@ -43,8 +43,8 @@
</appender> </appender>
<root level="INFO"> <root level="INFO">
<appender-ref ref="STDOUT" /> <appender-ref ref="STDOUT"/>
<appender-ref ref="FILE" /> <appender-ref ref="FILE"/>
</root> </root>
</configuration> </configuration>

View File

@@ -14,13 +14,10 @@
# See the License for the specific language governing permissions and # See the License for the specific language governing permissions and
# limitations under the License. # limitations under the License.
# #
# This file supports hot change, any change will be auto-reloaded without Dashboard restarting. # This file supports hot change, any change will be auto-reloaded without Dashboard restarting.
# Format: a user per line, username=password[,N] #N is optional, 0 (Normal User); 1 (Admin) # Format: a user per line, username=password[,N] #N is optional, 0 (Normal User); 1 (Admin)
# Define Admin # Define Admin
admin=admin,1 super=admin,1
# Define Users # Define Users
user1=user1 user1=user
user2=user2 user2=user

View File

@@ -17,12 +17,20 @@
package org.apache.rocketmq.dashboard; package org.apache.rocketmq.dashboard;
import java.lang.reflect.Field; import org.apache.rocketmq.remoting.protocol.body.ClusterInfo;
import java.util.ArrayList; import org.apache.rocketmq.remoting.protocol.route.BrokerData;
import java.util.List;
import org.mockito.internal.util.MockUtil; import org.mockito.internal.util.MockUtil;
import org.springframework.util.ReflectionUtils; import org.springframework.util.ReflectionUtils;
import java.lang.reflect.Field;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
public class BaseTest { public class BaseTest {
/** /**
* Inject the corresponding mock class automatically * Inject the corresponding mock class automatically
@@ -69,4 +77,20 @@ public class BaseTest {
ReflectionUtils.doWithFields(leafClass, fc); ReflectionUtils.doWithFields(leafClass, fc);
return fields; return fields;
} }
protected ClusterInfo getClusterInfo() {
ClusterInfo clusterInfo = new ClusterInfo();
Map<String, Set<String>> clusterAddrTable = new HashMap<>();
clusterAddrTable.put("DefaultCluster", new HashSet<>(Arrays.asList("broker-a")));
Map<String, BrokerData> brokerAddrTable = new HashMap<>();
BrokerData brokerData = new BrokerData();
brokerData.setBrokerName("broker-a");
HashMap<Long, String> brokerNameTable = new HashMap<>();
brokerNameTable.put(0L, "localhost:10911");
brokerData.setBrokerAddrs(brokerNameTable);
brokerAddrTable.put("broker-a", brokerData);
clusterInfo.setBrokerAddrTable(brokerAddrTable);
clusterInfo.setClusterAddrTable(clusterAddrTable);
return clusterInfo;
}
} }

View File

@@ -17,26 +17,45 @@
package org.apache.rocketmq.dashboard.admin; package org.apache.rocketmq.dashboard.admin;
import java.lang.reflect.Field;
import java.lang.reflect.Method;
import org.apache.commons.pool2.impl.GenericObjectPool; import org.apache.commons.pool2.impl.GenericObjectPool;
import org.apache.rocketmq.dashboard.aspect.admin.MQAdminAspect; import org.apache.rocketmq.dashboard.aspect.admin.MQAdminAspect;
import org.apache.rocketmq.dashboard.config.RMQConfigure;
import org.apache.rocketmq.tools.admin.DefaultMQAdminExt; import org.apache.rocketmq.tools.admin.DefaultMQAdminExt;
import org.apache.rocketmq.tools.admin.MQAdminExt; import org.apache.rocketmq.tools.admin.MQAdminExt;
import org.aspectj.lang.ProceedingJoinPoint; import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.reflect.MethodSignature; import org.aspectj.lang.reflect.MethodSignature;
import org.junit.Before;
import org.junit.Test; import org.junit.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.mockito.Mock;
import org.mockito.MockitoAnnotations;
import org.mockito.junit.jupiter.MockitoExtension;
import java.lang.reflect.Field;
import java.lang.reflect.Method;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.fail;
import static org.mockito.ArgumentMatchers.any; import static org.mockito.ArgumentMatchers.any;
import static org.mockito.Mockito.doNothing; import static org.mockito.Mockito.*;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.when;
public class MQAdminAspectTest { public class MQAdminAspectTest {
@Mock
private RMQConfigure rmqConfigure;
@Before
public void setUp() {
MockitoAnnotations.initMocks(this);
when(rmqConfigure.isLoginRequired()).thenReturn(false);
}
@Test @Test
public void testAroundMQAdminMethod() throws Throwable { public void testAroundMQAdminMethod() throws Throwable {
MQAdminAspect mqAdminAspect = new MQAdminAspect(); MQAdminAspect mqAdminAspect = new MQAdminAspect();
Field field = mqAdminAspect.getClass().getDeclaredField("rmqConfigure");
field.setAccessible(true);
field.set(mqAdminAspect, rmqConfigure);
ProceedingJoinPoint joinPoint = mock(ProceedingJoinPoint.class); ProceedingJoinPoint joinPoint = mock(ProceedingJoinPoint.class);
MethodSignature signature = mock(MethodSignature.class); MethodSignature signature = mock(MethodSignature.class);
Method method = mock(Method.class); Method method = mock(Method.class);
@@ -44,16 +63,39 @@ public class MQAdminAspectTest {
when(joinPoint.getSignature()).thenReturn(signature); when(joinPoint.getSignature()).thenReturn(signature);
GenericObjectPool<MQAdminExt> mqAdminExtPool = mock(GenericObjectPool.class); GenericObjectPool<MQAdminExt> mqAdminExtPool = mock(GenericObjectPool.class);
// 1. Mock borrowObject() 行为:第一次抛异常,第二次返回 DefaultMQAdminExt
when(mqAdminExtPool.borrowObject()) when(mqAdminExtPool.borrowObject())
.thenThrow(new RuntimeException("borrowObject exception")) .thenThrow(new RuntimeException("borrowObject exception"))
.thenReturn(new DefaultMQAdminExt()); .thenReturn(new DefaultMQAdminExt());
doNothing().doThrow(new RuntimeException("returnObject exception"))
// 2. Mock returnObject() 行为:第一次什么都不做,第二次抛异常
doNothing().when(mqAdminExtPool).returnObject(any());
doThrow(new RuntimeException("returnObject exception"))
.when(mqAdminExtPool).returnObject(any()); .when(mqAdminExtPool).returnObject(any());
Field field = mqAdminAspect.getClass().getDeclaredField("mqAdminExtPool");
// 3. 通过反射注入 Mock 对象
field = mqAdminAspect.getClass().getDeclaredField("mqAdminExtPool");
field.setAccessible(true); field.setAccessible(true);
field.set(mqAdminAspect, mqAdminExtPool); field.set(mqAdminAspect, mqAdminExtPool);
// exception
// 4. 第一次调用 aroundMQAdminMethod预期 borrowObject() 抛异常
try {
mqAdminAspect.aroundMQAdminMethod(joinPoint); mqAdminAspect.aroundMQAdminMethod(joinPoint);
fail("Expected RuntimeException but no exception was thrown");
} catch (RuntimeException e) {
assertEquals("borrowObject exception", e.getMessage());
}
// 5. 第二次调用 aroundMQAdminMethod预期 borrowObject() 成功,但 returnObject() 抛异常
try {
mqAdminAspect.aroundMQAdminMethod(joinPoint); mqAdminAspect.aroundMQAdminMethod(joinPoint);
fail("Expected RuntimeException but no exception was thrown");
} catch (RuntimeException e) {
assertEquals("returnObject exception", e.getMessage());
}
// 6. 验证 borrowObject() 和 returnObject() 各调用了两次
verify(mqAdminExtPool, times(2)).borrowObject();
verify(mqAdminExtPool, times(1)).returnObject(any());
} }
} }

View File

@@ -19,27 +19,25 @@ package org.apache.rocketmq.dashboard.admin;
import com.google.common.collect.Lists; import com.google.common.collect.Lists;
import com.google.common.collect.Sets; import com.google.common.collect.Sets;
import java.lang.reflect.Field;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Properties;
import java.util.Set;
import org.apache.rocketmq.client.QueryResult; import org.apache.rocketmq.client.QueryResult;
import org.apache.rocketmq.client.exception.MQBrokerException; import org.apache.rocketmq.client.exception.MQBrokerException;
import org.apache.rocketmq.client.impl.MQAdminImpl; import org.apache.rocketmq.client.impl.MQAdminImpl;
import org.apache.rocketmq.client.impl.MQClientAPIImpl; import org.apache.rocketmq.client.impl.MQClientAPIImpl;
import org.apache.rocketmq.client.impl.factory.MQClientInstance; import org.apache.rocketmq.client.impl.factory.MQClientInstance;
import org.apache.rocketmq.common.PlainAccessConfig;
import org.apache.rocketmq.common.TopicConfig; import org.apache.rocketmq.common.TopicConfig;
import org.apache.rocketmq.common.message.Message;
import org.apache.rocketmq.common.message.MessageExt;
import org.apache.rocketmq.common.message.MessageQueue;
import org.apache.rocketmq.dashboard.service.client.MQAdminExtImpl;
import org.apache.rocketmq.dashboard.service.client.MQAdminInstance;
import org.apache.rocketmq.dashboard.util.MockObjectUtil;
import org.apache.rocketmq.remoting.RemotingClient;
import org.apache.rocketmq.remoting.protocol.RemotingCommand;
import org.apache.rocketmq.remoting.protocol.RemotingSerializable;
import org.apache.rocketmq.remoting.protocol.ResponseCode;
import org.apache.rocketmq.remoting.protocol.admin.ConsumeStats; import org.apache.rocketmq.remoting.protocol.admin.ConsumeStats;
import org.apache.rocketmq.remoting.protocol.admin.RollbackStats; import org.apache.rocketmq.remoting.protocol.admin.RollbackStats;
import org.apache.rocketmq.remoting.protocol.admin.TopicStatsTable; import org.apache.rocketmq.remoting.protocol.admin.TopicStatsTable;
import org.apache.rocketmq.common.message.MessageExt;
import org.apache.rocketmq.common.message.MessageQueue;
import org.apache.rocketmq.remoting.protocol.ResponseCode;
import org.apache.rocketmq.remoting.protocol.body.BrokerStatsData; import org.apache.rocketmq.remoting.protocol.body.BrokerStatsData;
import org.apache.rocketmq.remoting.protocol.body.ClusterInfo; import org.apache.rocketmq.remoting.protocol.body.ClusterInfo;
import org.apache.rocketmq.remoting.protocol.body.ConsumeMessageDirectlyResult; import org.apache.rocketmq.remoting.protocol.body.ConsumeMessageDirectlyResult;
@@ -55,12 +53,6 @@ import org.apache.rocketmq.remoting.protocol.body.TopicConfigSerializeWrapper;
import org.apache.rocketmq.remoting.protocol.body.TopicList; import org.apache.rocketmq.remoting.protocol.body.TopicList;
import org.apache.rocketmq.remoting.protocol.route.TopicRouteData; import org.apache.rocketmq.remoting.protocol.route.TopicRouteData;
import org.apache.rocketmq.remoting.protocol.subscription.SubscriptionGroupConfig; import org.apache.rocketmq.remoting.protocol.subscription.SubscriptionGroupConfig;
import org.apache.rocketmq.dashboard.service.client.MQAdminExtImpl;
import org.apache.rocketmq.dashboard.service.client.MQAdminInstance;
import org.apache.rocketmq.dashboard.util.MockObjectUtil;
import org.apache.rocketmq.remoting.RemotingClient;
import org.apache.rocketmq.remoting.protocol.RemotingCommand;
import org.apache.rocketmq.remoting.protocol.RemotingSerializable;
import org.apache.rocketmq.store.stats.BrokerStatsManager; import org.apache.rocketmq.store.stats.BrokerStatsManager;
import org.apache.rocketmq.tools.admin.DefaultMQAdminExt; import org.apache.rocketmq.tools.admin.DefaultMQAdminExt;
import org.apache.rocketmq.tools.admin.DefaultMQAdminExtImpl; import org.apache.rocketmq.tools.admin.DefaultMQAdminExtImpl;
@@ -74,22 +66,25 @@ import org.mockito.Mock;
import org.mockito.junit.MockitoJUnitRunner; import org.mockito.junit.MockitoJUnitRunner;
import org.springframework.test.util.ReflectionTestUtils; import org.springframework.test.util.ReflectionTestUtils;
import java.lang.reflect.Field;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Properties;
import java.util.Set;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ConcurrentMap;
import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.assertThat;
import static org.junit.Assert.assertNotNull; import static org.junit.Assert.assertNotNull;
import static org.junit.Assert.assertNull; import static org.junit.Assert.assertNull;
import static org.junit.Assert.assertTrue; import static org.junit.Assert.assertTrue;
import static org.mockito.ArgumentMatchers.any; import static org.mockito.ArgumentMatchers.*;
import static org.mockito.ArgumentMatchers.anyBoolean;
import static org.mockito.ArgumentMatchers.anyInt;
import static org.mockito.ArgumentMatchers.anyLong;
import static org.mockito.ArgumentMatchers.anyMap;
import static org.mockito.ArgumentMatchers.anyString;
import static org.mockito.Mockito.doNothing; import static org.mockito.Mockito.doNothing;
import static org.mockito.Mockito.mock; import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.when; import static org.mockito.Mockito.when;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ConcurrentMap;
import static org.mockito.ArgumentMatchers.eq;
@RunWith(MockitoJUnitRunner.Silent.class) @RunWith(MockitoJUnitRunner.Silent.class)
public class MQAdminExtImplTest { public class MQAdminExtImplTest {
@@ -154,35 +149,6 @@ public class MQAdminExtImplTest {
mqAdminExtImpl.createAndUpdateTopicConfig(brokerAddr, new TopicConfig()); mqAdminExtImpl.createAndUpdateTopicConfig(brokerAddr, new TopicConfig());
} }
@Test
public void testDeletePlainAccessConfig() throws Exception {
assertNotNull(mqAdminExtImpl);
mqAdminExtImpl.deletePlainAccessConfig(brokerAddr, "rocketmq");
}
@Test
public void testUpdateGlobalWhiteAddrConfig() throws Exception {
assertNotNull(mqAdminExtImpl);
mqAdminExtImpl.updateGlobalWhiteAddrConfig(brokerAddr, "192.168.*.*");
}
@Test
public void testCreateAndUpdatePlainAccessConfig() throws Exception {
assertNotNull(mqAdminExtImpl);
mqAdminExtImpl.createAndUpdatePlainAccessConfig(brokerAddr, new PlainAccessConfig());
}
@Test
public void testExamineBrokerClusterAclVersionInfo() throws Exception {
assertNotNull(mqAdminExtImpl);
assertNull(mqAdminExtImpl.examineBrokerClusterAclVersionInfo(brokerAddr));
}
@Test
public void testExamineBrokerClusterAclConfig() throws Exception {
assertNotNull(mqAdminExtImpl);
assertNull(mqAdminExtImpl.examineBrokerClusterAclConfig(brokerAddr));
}
@Test @Test
public void testQueryConsumerStatus() throws Exception { public void testQueryConsumerStatus() throws Exception {
@@ -226,18 +192,15 @@ public class MQAdminExtImplTest {
public void testExamineTopicConfig() throws Exception { public void testExamineTopicConfig() throws Exception {
assertNotNull(mqAdminExtImpl); assertNotNull(mqAdminExtImpl);
// Create valid TopicConfigSerializeWrapper with topic_test entry // Create valid TopicConfigSerializeWrapper with topictest entry
TopicConfigSerializeWrapper wrapper = new TopicConfigSerializeWrapper();
ConcurrentMap<String, TopicConfig> topicConfigTable = new ConcurrentHashMap<>();
TopicConfig config = new TopicConfig(); TopicConfig config = new TopicConfig();
config.setTopicName("topic_test"); config.setTopicName("topic_test");
topicConfigTable.put("topic_test", config);
wrapper.setTopicConfigTable(topicConfigTable);
// Create successful response // Create successful response
RemotingCommand successResponse = RemotingCommand.createResponseCommand(null); RemotingCommand successResponse = RemotingCommand.createResponseCommand(null);
successResponse.setCode(ResponseCode.SUCCESS); successResponse.setCode(ResponseCode.SUCCESS);
successResponse.setBody(RemotingSerializable.encode(wrapper)); successResponse.setBody(RemotingSerializable.encode(config));
// Mock the remote invocation // Mock the remote invocation
when(remotingClient.invokeSync(eq(brokerAddr), any(RemotingCommand.class), anyLong())) when(remotingClient.invokeSync(eq(brokerAddr), any(RemotingCommand.class), anyLong()))
@@ -249,6 +212,7 @@ public class MQAdminExtImplTest {
Assert.assertEquals("topic_test", topicConfig.getTopicName()); Assert.assertEquals("topic_test", topicConfig.getTopicName());
} }
@Test @Test
public void testExamineTopicStats() throws Exception { public void testExamineTopicStats() throws Exception {
assertNotNull(mqAdminExtImpl); assertNotNull(mqAdminExtImpl);
@@ -257,7 +221,7 @@ public class MQAdminExtImplTest {
} }
TopicStatsTable topicStatsTable = mqAdminExtImpl.examineTopicStats("topic_test"); TopicStatsTable topicStatsTable = mqAdminExtImpl.examineTopicStats("topic_test");
Assert.assertNotNull(topicStatsTable); Assert.assertNotNull(topicStatsTable);
Assert.assertEquals(topicStatsTable.getOffsetTable().size(), 1); Assert.assertEquals(1, topicStatsTable.getOffsetTable().size());
} }
@Test @Test
@@ -347,7 +311,7 @@ public class MQAdminExtImplTest {
public void testGetNameServerAddressList() { public void testGetNameServerAddressList() {
assertNotNull(mqAdminExtImpl); assertNotNull(mqAdminExtImpl);
{ {
when(defaultMQAdminExt.getNameServerAddressList()).thenReturn(Lists.asList("127.0.0.1:9876", new String[] {"127.0.0.2:9876"})); when(defaultMQAdminExt.getNameServerAddressList()).thenReturn(Lists.asList("127.0.0.1:9876", new String[]{"127.0.0.2:9876"}));
} }
List<String> list = mqAdminExtImpl.getNameServerAddressList(); List<String> list = mqAdminExtImpl.getNameServerAddressList();
Assert.assertEquals(list.size(), 2); Assert.assertEquals(list.size(), 2);
@@ -535,12 +499,10 @@ public class MQAdminExtImplTest {
public void testConsumeMessageDirectly() throws Exception { public void testConsumeMessageDirectly() throws Exception {
assertNotNull(mqAdminExtImpl); assertNotNull(mqAdminExtImpl);
{ {
when(defaultMQAdminExt.consumeMessageDirectly(anyString(), anyString(), anyString())).thenReturn(new ConsumeMessageDirectlyResult());
when(defaultMQAdminExt.consumeMessageDirectly(anyString(), anyString(), anyString(), anyString())).thenReturn(new ConsumeMessageDirectlyResult()); when(defaultMQAdminExt.consumeMessageDirectly(anyString(), anyString(), anyString(), anyString())).thenReturn(new ConsumeMessageDirectlyResult());
} }
ConsumeMessageDirectlyResult result1 = mqAdminExtImpl.consumeMessageDirectly("group_test", "", "7F000001ACC018B4AAC2116AF6500000");
ConsumeMessageDirectlyResult result2 = mqAdminExtImpl.consumeMessageDirectly("group_test", "", "topic_test", "7F000001ACC018B4AAC2116AF6500000"); ConsumeMessageDirectlyResult result2 = mqAdminExtImpl.consumeMessageDirectly("group_test", "", "topic_test", "7F000001ACC018B4AAC2116AF6500000");
Assert.assertNotNull(result1);
Assert.assertNotNull(result2); Assert.assertNotNull(result2);
} }
@@ -616,15 +578,6 @@ public class MQAdminExtImplTest {
Assert.assertEquals(storeTime, 1628495765398L); Assert.assertEquals(storeTime, 1628495765398L);
} }
@Test
public void testViewMessage() throws Exception {
assertNotNull(mqAdminExtImpl);
{
when(defaultMQAdminExt.viewMessage(anyString())).thenReturn(new MessageExt());
}
MessageExt messageExt = mqAdminExtImpl.viewMessage("7F000001ACC018B4AAC2116AF6500000");
Assert.assertNotNull(messageExt);
}
@Test @Test
public void testQueryMessage() throws Exception { public void testQueryMessage() throws Exception {
@@ -666,15 +619,6 @@ public class MQAdminExtImplTest {
Assert.assertNotNull(timeSpans); Assert.assertNotNull(timeSpans);
} }
@Test
public void testViewMessage2() throws Exception {
assertNotNull(mqAdminExtImpl);
{
when(MQAdminInstance.threadLocalMqClientInstance().getMQAdminImpl()).thenReturn(mock(MQAdminImpl.class));
when(defaultMQAdminExt.viewMessage(anyString())).thenThrow(new RuntimeException("viewMessage exception"));
}
mqAdminExtImpl.viewMessage("topic_test", "7F000001ACC018B4AAC2116AF6500000");
}
@Test @Test
public void testGetBrokerConfig() throws Exception { public void testGetBrokerConfig() throws Exception {
@@ -788,7 +732,6 @@ public class MQAdminExtImplTest {
@Test @Test
public void testResumeCheckHalfMessage() throws Exception { public void testResumeCheckHalfMessage() throws Exception {
assertNotNull(mqAdminExtImpl); assertNotNull(mqAdminExtImpl);
Assert.assertFalse(mqAdminExtImpl.resumeCheckHalfMessage("7F000001ACC018B4AAC2116AF6500000"));
Assert.assertFalse(mqAdminExtImpl.resumeCheckHalfMessage("topic_test", "7F000001ACC018B4AAC2116AF6500000")); Assert.assertFalse(mqAdminExtImpl.resumeCheckHalfMessage("topic_test", "7F000001ACC018B4AAC2116AF6500000"));
} }

View File

@@ -18,56 +18,41 @@
package org.apache.rocketmq.dashboard.config; package org.apache.rocketmq.dashboard.config;
import org.apache.rocketmq.dashboard.BaseTest; import org.apache.rocketmq.dashboard.BaseTest;
import org.apache.rocketmq.dashboard.interceptor.AuthInterceptor;
import org.assertj.core.util.Lists;
import org.junit.Before;
import org.junit.Test;
import org.junit.jupiter.api.Assertions;
import org.mockito.InjectMocks;
import org.mockito.Mock;
import org.mockito.Mockito;
import org.mockito.MockitoAnnotations;
import org.springframework.context.support.ClassPathXmlApplicationContext;
import org.springframework.web.method.support.HandlerMethodArgumentResolver;
import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
import org.springframework.web.servlet.config.annotation.ViewControllerRegistry;
import java.util.List;
public class AuthWebMVCConfigurerAdapterTest extends BaseTest { public class AuthWebMVCConfigurerAdapterTest extends BaseTest {
@InjectMocks // @InjectMocks
private AuthWebMVCConfigurerAdapter authWebMVCConfigurerAdapter; // private AuthWebMVCConfigurerAdapter authWebMVCConfigurerAdapter;
//
@Mock // @Mock
private RMQConfigure configure; // private RMQConfigure configure;
//
@Mock // @Mock
private AuthInterceptor authInterceptor; // private AuthInterceptor authInterceptor;
//
@Before // @Before
public void init() throws Exception { // public void init() throws Exception {
MockitoAnnotations.initMocks(this); // MockitoAnnotations.initMocks(this);
} // }
//
//
@Test // @Test
public void addInterceptors() { // public void addInterceptors() {
Mockito.when(configure.isLoginRequired()).thenReturn(true); // Mockito.when(configure.isLoginRequired()).thenReturn(true);
InterceptorRegistry registry = new InterceptorRegistry(); // InterceptorRegistry registry = new InterceptorRegistry();
Assertions.assertDoesNotThrow(() -> authWebMVCConfigurerAdapter.addInterceptors(registry)); // Assertions.assertDoesNotThrow(() -> authWebMVCConfigurerAdapter.addInterceptors(registry));
} // }
//
@Test // @Test
public void addArgumentResolvers() { // public void addArgumentResolvers() {
List<HandlerMethodArgumentResolver> argumentResolvers = Lists.newArrayList(); // List<HandlerMethodArgumentResolver> argumentResolvers = Lists.newArrayList();
authWebMVCConfigurerAdapter.addArgumentResolvers(argumentResolvers); // authWebMVCConfigurerAdapter.addArgumentResolvers(argumentResolvers);
Assertions.assertEquals(1, argumentResolvers.size()); // Assertions.assertEquals(1, argumentResolvers.size());
} // }
//
@Test // @Test
public void addViewControllers() { // public void addViewControllers() {
ViewControllerRegistry registry = new ViewControllerRegistry(new ClassPathXmlApplicationContext()); // ViewControllerRegistry registry = new ViewControllerRegistry(new ClassPathXmlApplicationContext());
Assertions.assertDoesNotThrow(() -> authWebMVCConfigurerAdapter.addViewControllers(registry)); // Assertions.assertDoesNotThrow(() -> authWebMVCConfigurerAdapter.addViewControllers(registry));
} // }
} }

View File

@@ -16,11 +16,12 @@
*/ */
package org.apache.rocketmq.dashboard.config; package org.apache.rocketmq.dashboard.config;
import org.junit.Assert;
import org.junit.Test;
import java.util.concurrent.CountDownLatch; import java.util.concurrent.CountDownLatch;
import java.util.concurrent.ExecutorService; import java.util.concurrent.ExecutorService;
import java.util.concurrent.atomic.AtomicInteger; import java.util.concurrent.atomic.AtomicInteger;
import org.junit.Assert;
import org.junit.Test;
public class CollectExecutorConfigTest { public class CollectExecutorConfigTest {

View File

@@ -18,13 +18,14 @@
package org.apache.rocketmq.dashboard.config; package org.apache.rocketmq.dashboard.config;
import com.google.common.collect.Lists; import com.google.common.collect.Lists;
import java.io.File;
import org.junit.Assert; import org.junit.Assert;
import org.junit.Test; import org.junit.Test;
import org.springframework.boot.web.server.ErrorPage; import org.springframework.boot.web.server.ErrorPage;
import org.springframework.boot.web.server.ErrorPageRegistrar; import org.springframework.boot.web.server.ErrorPageRegistrar;
import org.springframework.boot.web.server.ErrorPageRegistry; import org.springframework.boot.web.server.ErrorPageRegistry;
import java.io.File;
public class RMQConfigureTest { public class RMQConfigureTest {
private RMQConfigure rmqConfigure = new RMQConfigure(); private RMQConfigure rmqConfigure = new RMQConfigure();
@@ -40,7 +41,8 @@ public class RMQConfigureTest {
rmqConfigure.setLoginRequired(true); rmqConfigure.setLoginRequired(true);
rmqConfigure.setNamesrvAddr("127.0.0.1:9876"); rmqConfigure.setNamesrvAddr("127.0.0.1:9876");
rmqConfigure.setTimeoutMillis(3000L); rmqConfigure.setTimeoutMillis(3000L);
rmqConfigure.setNamesrvAddrs(Lists.asList("127.0.0.1:9876", new String[] {"127.0.0.2:9876"})); rmqConfigure.setNamesrvAddrs(Lists.asList("127.0.0.1:9876", new String[]{"127.0.0.2:9876"}));
rmqConfigure.setAuthMode("file");
} }
@Test @Test
@@ -58,6 +60,7 @@ public class RMQConfigureTest {
Assert.assertEquals(rmqConfigure.getNamesrvAddr(), "127.0.0.1:9876"); Assert.assertEquals(rmqConfigure.getNamesrvAddr(), "127.0.0.1:9876");
Assert.assertEquals(rmqConfigure.getNamesrvAddrs().size(), 2); Assert.assertEquals(rmqConfigure.getNamesrvAddrs().size(), 2);
Assert.assertEquals(rmqConfigure.getTimeoutMillis().longValue(), 3000L); Assert.assertEquals(rmqConfigure.getTimeoutMillis().longValue(), 3000L);
Assert.assertEquals(rmqConfigure.getAuthMode(), "file");
ErrorPageRegistrar registrar = rmqConfigure.errorPageRegistrar(); ErrorPageRegistrar registrar = rmqConfigure.errorPageRegistrar();
registrar.registerErrorPages(new ErrorPageRegistry() { registrar.registerErrorPages(new ErrorPageRegistry() {
@Override @Override

View File

@@ -16,353 +16,228 @@
*/ */
package org.apache.rocketmq.dashboard.controller; package org.apache.rocketmq.dashboard.controller;
import com.alibaba.fastjson.JSON;
import com.google.common.collect.Lists; import org.apache.rocketmq.auth.authentication.enums.UserStatus;
import java.util.List; import org.apache.rocketmq.auth.authentication.enums.UserType;
import org.apache.rocketmq.common.AclConfig; import org.apache.rocketmq.auth.authorization.enums.Decision;
import org.apache.rocketmq.common.PlainAccessConfig; import org.apache.rocketmq.dashboard.model.Policy;
import org.apache.rocketmq.remoting.protocol.body.ClusterInfo; import org.apache.rocketmq.dashboard.model.PolicyRequest;
import org.apache.rocketmq.dashboard.model.request.AclRequest; import org.apache.rocketmq.dashboard.model.request.UserCreateRequest;
import org.apache.rocketmq.dashboard.model.request.UserInfoParam;
import org.apache.rocketmq.dashboard.model.request.UserUpdateRequest;
import org.apache.rocketmq.dashboard.service.impl.AclServiceImpl; import org.apache.rocketmq.dashboard.service.impl.AclServiceImpl;
import org.apache.rocketmq.dashboard.util.MockObjectUtil; import org.apache.rocketmq.dashboard.support.GlobalExceptionHandler;
import org.apache.rocketmq.remoting.protocol.body.AclInfo;
import org.apache.rocketmq.remoting.protocol.body.UserInfo;
import org.junit.Before; import org.junit.Before;
import org.junit.Test; import org.junit.Test;
import org.mockito.InjectMocks; import org.mockito.InjectMocks;
import org.mockito.Spy; import org.mockito.Mock;
import org.springframework.http.MediaType; import org.mockito.MockitoAnnotations;
import org.springframework.test.web.servlet.request.MockMvcRequestBuilders; import org.springframework.test.web.servlet.setup.MockMvcBuilders;
import static org.mockito.ArgumentMatchers.any; import java.util.Arrays;
import static org.mockito.ArgumentMatchers.anyString; import java.util.List;
import static org.mockito.Mockito.doNothing;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.mockito.Mockito.times;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when; import static org.mockito.Mockito.when;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;
public class AclControllerTest extends BaseControllerTest { public class AclControllerTest extends BaseControllerTest {
@Mock
private AclServiceImpl aclService;
@InjectMocks @InjectMocks
private AclController aclController; private AclController aclController;
@Spy
private AclServiceImpl aclService;
@Before @Before
public void init() throws Exception { public void init() {
AclConfig aclConfig = MockObjectUtil.createAclConfig(); MockitoAnnotations.initMocks(this);
when(mqAdminExt.examineBrokerClusterAclConfig(anyString())).thenReturn(aclConfig); mockMvc = MockMvcBuilders.standaloneSetup(aclController).setControllerAdvice(GlobalExceptionHandler.class).build();
ClusterInfo clusterInfo = MockObjectUtil.createClusterInfo(); }
when(mqAdminExt.examineBrokerClusterInfo()).thenReturn(clusterInfo);
doNothing().when(mqAdminExt).createAndUpdatePlainAccessConfig(anyString(), any(PlainAccessConfig.class));
doNothing().when(mqAdminExt).deletePlainAccessConfig(anyString(), anyString()); @Test
doNothing().when(mqAdminExt).updateGlobalWhiteAddrConfig(anyString(), anyString()); public void testListUsers() {
// Prepare test data
String brokerAddress = "localhost:10911";
List<UserInfo> expectedUsers = Arrays.asList(
UserInfo.of("user1", "password1", "super"),
UserInfo.of("user2", "password2", "super")
);
// Mock service behavior
when(aclService.listUsers(brokerAddress)).thenReturn(expectedUsers);
// Call controller method
List<UserInfo> result = aclController.listUsers(brokerAddress);
// Verify
assertEquals(expectedUsers, result);
verify(aclService, times(1)).listUsers(brokerAddress);
} }
@Test @Test
public void testIsEnableAcl() throws Exception { public void testListUsersWithoutBrokerAddress() {
final String url = "/acl/enable.query"; // Prepare test data
// 1. disable acl. List<UserInfo> expectedUsers = Arrays.asList(
requestBuilder = MockMvcRequestBuilders.get(url); UserInfo.of("user1", "password1", "super")
perform = mockMvc.perform(requestBuilder); );
perform.andExpect(status().isOk())
.andExpect(jsonPath("$.data").value(false));
// 2.enable acl. // Mock service behavior
super.mockRmqConfigure(); when(aclService.listUsers(null)).thenReturn(expectedUsers);
perform = mockMvc.perform(requestBuilder); // Call controller method
perform.andExpect(status().isOk()) List<UserInfo> result = aclController.listUsers(null);
.andExpect(jsonPath("$.data").value(true)); // Verify
assertEquals(expectedUsers, result);
verify(aclService, times(1)).listUsers(null);
} }
@Test @Test
public void testGetAclConfig() throws Exception { public void testListAcls() {
final String url = "/acl/config.query"; // Prepare test data
String brokerAddress = "localhost:9092";
String searchParam = "user1";
Object expectedAcls = Arrays.asList(
AclInfo.of("user1", List.of("READ", "test"), List.of("TOPIC:test"), List.of("localhost:10911"), Decision.ALLOW.getName())
);
// 1. broker addr table is not empty. // Mock service behavior
ClusterInfo clusterInfo = MockObjectUtil.createClusterInfo(); when(aclService.listAcls(brokerAddress, searchParam)).thenReturn(expectedAcls);
when(mqAdminExt.examineBrokerClusterInfo()).thenReturn(clusterInfo);
requestBuilder = MockMvcRequestBuilders.get(url);
perform = mockMvc.perform(requestBuilder);
perform.andExpect(status().isOk())
.andExpect(jsonPath("$.data").isMap())
.andExpect(jsonPath("$.data.globalWhiteAddrs").isNotEmpty())
.andExpect(jsonPath("$.data.plainAccessConfigs").isNotEmpty())
.andExpect(jsonPath("$.data.plainAccessConfigs[0].secretKey").isNotEmpty());
// 2. broker addr table is empty. // Call controller method
clusterInfo.getBrokerAddrTable().clear(); Object result = aclController.listAcls(brokerAddress, searchParam);
perform = mockMvc.perform(requestBuilder);
perform.andExpect(status().isOk())
.andExpect(jsonPath("$.data").isMap())
.andExpect(jsonPath("$.data.globalWhiteAddrs").isEmpty())
.andExpect(jsonPath("$.data.plainAccessConfigs").isEmpty());
// 3. login required and user info is null. // Verify
when(configure.isLoginRequired()).thenReturn(true); assertEquals(expectedAcls, result);
when(mqAdminExt.examineBrokerClusterInfo()).thenReturn(MockObjectUtil.createClusterInfo()); verify(aclService, times(1)).listAcls(brokerAddress, searchParam);
perform = mockMvc.perform(requestBuilder);
perform.andExpect(status().isOk())
.andExpect(jsonPath("$.data").isMap())
.andExpect(jsonPath("$.data.globalWhiteAddrs").isNotEmpty())
.andExpect(jsonPath("$.data.plainAccessConfigs").isNotEmpty())
.andExpect(jsonPath("$.data.plainAccessConfigs[0].secretKey").isEmpty());
// 4. login required, but user is not admin. emmmm, Mockito may can not mock static method.
} }
@Test @Test
public void testAddAclConfig() throws Exception { public void testCreateAcl() {
final String url = "/acl/add.do"; // Prepare test data
PlainAccessConfig accessConfig = new PlainAccessConfig(); PolicyRequest request = new PolicyRequest();
requestBuilder = MockMvcRequestBuilders.post(url); request.setBrokerAddress("localhost:9092");
requestBuilder.contentType(MediaType.APPLICATION_JSON_UTF8); request.setSubject("user1");
request.setPolicies(List.of(
new Policy()
));
// 1. access key is null. // Call controller method
requestBuilder.content(JSON.toJSONString(accessConfig)); Object result = aclController.createAcl(request);
perform = mockMvc.perform(requestBuilder);
perform.andExpect(status().isOk())
.andExpect(jsonPath("$.status").value(-1))
.andExpect(jsonPath("$.errMsg").exists());
// 2. secret key is null. // Verify
accessConfig.setAccessKey("test-access-key"); assertEquals(true, result);
requestBuilder.content(JSON.toJSONString(accessConfig)); verify(aclService, times(1)).createAcl(request);
perform = mockMvc.perform(requestBuilder);
perform.andExpect(status().isOk())
.andExpect(jsonPath("$.status").value(-1))
.andExpect(jsonPath("$.errMsg").exists());
ClusterInfo clusterInfo = MockObjectUtil.createClusterInfo();
when(mqAdminExt.examineBrokerClusterInfo()).thenReturn(clusterInfo);
// 3. add if the access key not exist.
accessConfig.setSecretKey("12345678");
requestBuilder.content(JSON.toJSONString(accessConfig));
perform = mockMvc.perform(requestBuilder);
perform.andExpect(status().isOk())
.andExpect(jsonPath("$.status").value(0));
// 4. add failed if the access key is existed.
accessConfig.setAccessKey("rocketmq2");
requestBuilder.content(JSON.toJSONString(accessConfig));
perform = mockMvc.perform(requestBuilder);
perform.andExpect(status().isOk())
.andExpect(jsonPath("$.status").value(-1))
.andExpect(jsonPath("$.errMsg").exists());
// 5. add failed if there is no alive broker.
clusterInfo.getBrokerAddrTable().clear();
accessConfig.setAccessKey("test-access-key");
requestBuilder.content(JSON.toJSONString(accessConfig));
perform = mockMvc.perform(requestBuilder);
perform.andExpect(status().isOk())
.andExpect(jsonPath("$.status").value(-1))
.andExpect(jsonPath("$.errMsg").exists());
} }
@Test @Test
public void testDeleteAclConfig() throws Exception { public void testDeleteUser() {
final String url = "/acl/delete.do"; // Prepare test data
PlainAccessConfig accessConfig = new PlainAccessConfig(); String brokerAddress = "localhost:9092";
requestBuilder = MockMvcRequestBuilders.post(url); String username = "user1";
requestBuilder.contentType(MediaType.APPLICATION_JSON_UTF8);
// 1. access key is null. // Call controller method
requestBuilder.content(JSON.toJSONString(accessConfig)); Object result = aclController.deleteUser(brokerAddress, username);
perform = mockMvc.perform(requestBuilder);
perform.andExpect(status().isOk())
.andExpect(jsonPath("$.status").value(-1))
.andExpect(jsonPath("$.errMsg").exists());
// 2. access key is not null. // Verify
accessConfig.setAccessKey("rocketmq"); assertEquals(true, result);
requestBuilder.content(JSON.toJSONString(accessConfig)); verify(aclService, times(1)).deleteUser(brokerAddress, username);
perform = mockMvc.perform(requestBuilder);
perform.andExpect(status().isOk())
.andExpect(jsonPath("$.status").value(0));
} }
@Test @Test
public void testUpdateAclConfig() throws Exception { public void testDeleteUserWithoutBrokerAddress() {
final String url = "/acl/update.do"; // Prepare test data
PlainAccessConfig accessConfig = new PlainAccessConfig(); String username = "user1";
requestBuilder = MockMvcRequestBuilders.post(url);
requestBuilder.contentType(MediaType.APPLICATION_JSON_UTF8);
// 1. secret key is null. // Call controller method
accessConfig.setAccessKey("rocketmq"); Object result = aclController.deleteUser(null, username);
requestBuilder.content(JSON.toJSONString(accessConfig));
perform = mockMvc.perform(requestBuilder);
perform.andExpect(status().isOk())
.andExpect(jsonPath("$.status").value(-1))
.andExpect(jsonPath("$.errMsg").exists());
// 2. update. // Verify
accessConfig.setSecretKey("abcdefghjkl"); assertEquals(true, result);
requestBuilder.content(JSON.toJSONString(accessConfig)); verify(aclService, times(1)).deleteUser(null, username);
perform = mockMvc.perform(requestBuilder);
perform.andExpect(status().isOk())
.andExpect(jsonPath("$.status").value(0));
} }
@Test @Test
public void testAddAclTopicConfig() throws Exception { public void testUpdateUser() {
final String url = "/acl/topic/add.do"; // Prepare test data
AclRequest request = new AclRequest(); UserUpdateRequest request = new UserUpdateRequest();
request.setConfig(createDefaultPlainAccessConfig()); request.setBrokerAddress("localhost:9092");
request.setUserInfo(new UserInfoParam("user1", "newPassword", UserStatus.ENABLE.getName(), UserType.SUPER.getName()));
// 1. if not exist. // Call controller method
request.setTopicPerm("test_topic=PUB"); Object result = aclController.updateUser(request);
requestBuilder = MockMvcRequestBuilders.post(url);
requestBuilder.contentType(MediaType.APPLICATION_JSON_UTF8);
requestBuilder.content(JSON.toJSONString(request));
perform = mockMvc.perform(requestBuilder);
perform.andExpect(status().isOk())
.andExpect(jsonPath("$.status").value(0));
// 2. if exist. // Verify
request.setTopicPerm("topicA=PUB"); assertEquals(true, result);
requestBuilder.content(JSON.toJSONString(request)); verify(aclService, times(1)).updateUser(request.getBrokerAddress(), request.getUserInfo());
perform = mockMvc.perform(requestBuilder);
perform.andExpect(status().isOk())
.andExpect(jsonPath("$.status").value(0));
// 3. if access key not exist.
request.getConfig().setAccessKey("test_access_key123");
requestBuilder.content(JSON.toJSONString(request));
perform = mockMvc.perform(requestBuilder);
perform.andExpect(status().isOk())
.andExpect(jsonPath("$.status").value(0));
} }
@Test @Test
public void testAddAclGroupConfig() throws Exception { public void testCreateUser() {
final String url = "/acl/group/add.do"; // Prepare test data
AclRequest request = new AclRequest(); UserCreateRequest request = new UserCreateRequest();
request.setConfig(createDefaultPlainAccessConfig()); request.setBrokerAddress("localhost:9092");
request.setUserInfo(new UserInfoParam("user1", "newPassword", UserStatus.ENABLE.getName(), UserType.SUPER.getName()));
// 1. if not exist. // Call controller method
request.setGroupPerm("test_consumer=PUB|SUB"); Object result = aclController.createUser(request);
requestBuilder = MockMvcRequestBuilders.post(url);
requestBuilder.contentType(MediaType.APPLICATION_JSON_UTF8);
requestBuilder.content(JSON.toJSONString(request));
perform = mockMvc.perform(requestBuilder);
perform.andExpect(status().isOk())
.andExpect(jsonPath("$.status").value(0));
// 2. if exist. // Verify
request.setGroupPerm("groupA=PUB|SUB"); assertEquals(true, result);
requestBuilder.content(JSON.toJSONString(request)); verify(aclService, times(1)).createUser(request.getBrokerAddress(), request.getUserInfo());
perform = mockMvc.perform(requestBuilder);
perform.andExpect(status().isOk())
.andExpect(jsonPath("$.status").value(0));
// 3. if access key not exist.
request.getConfig().setAccessKey("test_access_key123");
requestBuilder.content(JSON.toJSONString(request));
perform = mockMvc.perform(requestBuilder);
perform.andExpect(status().isOk())
.andExpect(jsonPath("$.status").value(0));
} }
@Test @Test
public void testDeletePermConfig() throws Exception { public void testDeleteAcl() {
final String url = "/acl/perm/delete.do"; // Prepare test data
AclRequest request = new AclRequest(); String brokerAddress = "localhost:9092";
request.setConfig(createDefaultPlainAccessConfig()); String subject = "user1";
requestBuilder = MockMvcRequestBuilders.post(url); String resource = "TOPIC:test";
requestBuilder.contentType(MediaType.APPLICATION_JSON_UTF8);
requestBuilder.content(JSON.toJSONString(request));
perform = mockMvc.perform(requestBuilder);
perform.andExpect(status().isOk())
.andExpect(jsonPath("$.status").value(0));
// if access key not exist. // Call controller method
request.getConfig().setAccessKey("test_access_key123"); Object result = aclController.deleteAcl(brokerAddress, subject, resource);
requestBuilder.content(JSON.toJSONString(request));
perform = mockMvc.perform(requestBuilder); // Verify
perform.andExpect(status().isOk()) assertEquals(true, result);
.andExpect(jsonPath("$.status").value(0)); verify(aclService, times(1)).deleteAcl(brokerAddress, subject, resource);
} }
@Test @Test
public void testSyncConfig() throws Exception { public void testDeleteAclWithoutBrokerAddressAndResource() {
final String url = "/acl/sync.do"; // Prepare test data
requestBuilder = MockMvcRequestBuilders.post(url); String subject = "user1";
requestBuilder.contentType(MediaType.APPLICATION_JSON_UTF8);
requestBuilder.content(JSON.toJSONString(createDefaultPlainAccessConfig())); // Call controller method
perform = mockMvc.perform(requestBuilder); Object result = aclController.deleteAcl(null, subject, null);
perform.andExpect(status().isOk())
.andExpect(jsonPath("$.status").value(0)); // Verify
assertEquals(true, result);
verify(aclService, times(1)).deleteAcl(null, subject, null);
} }
@Test @Test
public void testAddWhiteList() throws Exception { public void testUpdateAcl() {
final String url = "/acl/white/list/add.do"; // Prepare test data
List<String> whiteList = Lists.newArrayList("192.168.0.1"); PolicyRequest request = new PolicyRequest();
request.setBrokerAddress("localhost:9092");
request.setSubject("user1");
request.setPolicies(List.of(
new Policy()
));
// 1. if global white list is not null. // Call controller method
requestBuilder = MockMvcRequestBuilders.post(url); Object result = aclController.updateAcl(request);
requestBuilder.contentType(MediaType.APPLICATION_JSON_UTF8);
requestBuilder.content(JSON.toJSONString(whiteList));
perform = mockMvc.perform(requestBuilder);
perform.andExpect(status().isOk())
.andExpect(jsonPath("$.status").value(0));
// 2. if global white list is null. // Verify
AclConfig aclConfig = MockObjectUtil.createAclConfig(); assertEquals(true, result);
aclConfig.setGlobalWhiteAddrs(null); verify(aclService, times(1)).updateAcl(request);
when(mqAdminExt.examineBrokerClusterAclConfig(anyString())).thenReturn(aclConfig);
perform = mockMvc.perform(requestBuilder);
perform.andExpect(status().isOk())
.andExpect(jsonPath("$.status").value(0));
} }
@Test @Override
public void testDeleteWhiteAddr() throws Exception { protected Object getTestController() {
final String url = "/acl/white/list/delete.do";
requestBuilder = MockMvcRequestBuilders.delete(url);
requestBuilder.param("request", "localhost");
perform = mockMvc.perform(requestBuilder);
perform.andExpect(status().isOk())
.andExpect(jsonPath("$.status").value(0));
}
@Test
public void testSynchronizeWhiteList() throws Exception {
final String url = "/acl/white/list/sync.do";
List<String> whiteList = Lists.newArrayList();
// 1. if white list for syncing is empty.
requestBuilder = MockMvcRequestBuilders.post(url);
requestBuilder.contentType(MediaType.APPLICATION_JSON_UTF8);
requestBuilder.content(JSON.toJSONString(whiteList));
perform = mockMvc.perform(requestBuilder);
perform.andExpect(status().isOk())
.andExpect(jsonPath("$.status").value(-1))
.andExpect(jsonPath("$.errMsg").exists());
// 2. if white list for syncing is not empty.
whiteList.add("localhost");
requestBuilder.content(JSON.toJSONString(whiteList));
perform = mockMvc.perform(requestBuilder);
perform.andExpect(status().isOk())
.andExpect(jsonPath("$.status").value(0));
}
@Override protected Object getTestController() {
return aclController; return aclController;
} }
private PlainAccessConfig createDefaultPlainAccessConfig() {
PlainAccessConfig config = new PlainAccessConfig();
config.setAdmin(false);
config.setAccessKey("rocketmq");
config.setSecretKey("123456789");
config.setDefaultGroupPerm("SUB");
config.setDefaultTopicPerm("DENY");
config.setTopicPerms(Lists.newArrayList("topicA=DENY", "topicB=PUB|SUB"));
config.setGroupPerms(Lists.newArrayList("groupA=DENY", "groupB=PUB|SUB"));
return config;
}
} }

View File

@@ -23,6 +23,8 @@ import org.apache.rocketmq.dashboard.config.RMQConfigure;
import org.apache.rocketmq.dashboard.support.GlobalExceptionHandler; import org.apache.rocketmq.dashboard.support.GlobalExceptionHandler;
import org.apache.rocketmq.dashboard.support.GlobalRestfulResponseBodyAdvice; import org.apache.rocketmq.dashboard.support.GlobalRestfulResponseBodyAdvice;
import org.apache.rocketmq.dashboard.util.MyPrintingResultHandler; import org.apache.rocketmq.dashboard.util.MyPrintingResultHandler;
import org.apache.rocketmq.remoting.protocol.body.ClusterInfo;
import org.apache.rocketmq.remoting.protocol.route.BrokerData;
import org.apache.rocketmq.tools.admin.MQAdminExt; import org.apache.rocketmq.tools.admin.MQAdminExt;
import org.junit.Before; import org.junit.Before;
import org.mockito.Mock; import org.mockito.Mock;
@@ -32,6 +34,12 @@ import org.springframework.test.web.servlet.ResultActions;
import org.springframework.test.web.servlet.request.MockHttpServletRequestBuilder; import org.springframework.test.web.servlet.request.MockHttpServletRequestBuilder;
import org.springframework.test.web.servlet.setup.MockMvcBuilders; import org.springframework.test.web.servlet.setup.MockMvcBuilders;
import java.util.Arrays;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Map;
import java.util.Set;
import static org.mockito.Mockito.when; import static org.mockito.Mockito.when;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath; import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;

View File

@@ -16,17 +16,18 @@
*/ */
package org.apache.rocketmq.dashboard.controller; package org.apache.rocketmq.dashboard.controller;
import java.util.HashMap;
import java.util.Properties;
import org.apache.rocketmq.remoting.protocol.body.ClusterInfo;
import org.apache.rocketmq.remoting.protocol.body.KVTable;
import org.apache.rocketmq.dashboard.service.impl.ClusterServiceImpl; import org.apache.rocketmq.dashboard.service.impl.ClusterServiceImpl;
import org.apache.rocketmq.dashboard.util.MockObjectUtil; import org.apache.rocketmq.dashboard.util.MockObjectUtil;
import org.apache.rocketmq.remoting.protocol.body.ClusterInfo;
import org.apache.rocketmq.remoting.protocol.body.KVTable;
import org.junit.Test; import org.junit.Test;
import org.mockito.InjectMocks; import org.mockito.InjectMocks;
import org.mockito.Spy; import org.mockito.Spy;
import org.springframework.test.web.servlet.request.MockMvcRequestBuilders; import org.springframework.test.web.servlet.request.MockMvcRequestBuilders;
import java.util.HashMap;
import java.util.Properties;
import static org.mockito.ArgumentMatchers.anyString; import static org.mockito.ArgumentMatchers.anyString;
import static org.mockito.Mockito.when; import static org.mockito.Mockito.when;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath; import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath;

View File

@@ -19,44 +19,43 @@ package org.apache.rocketmq.dashboard.controller;
import com.alibaba.fastjson.JSON; import com.alibaba.fastjson.JSON;
import com.google.common.collect.Lists; import com.google.common.collect.Lists;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import org.apache.rocketmq.client.exception.MQClientException; import org.apache.rocketmq.client.exception.MQClientException;
import org.apache.rocketmq.common.message.MessageQueue;
import org.apache.rocketmq.dashboard.model.QueueStatInfo;
import org.apache.rocketmq.dashboard.model.TopicConsumerInfo;
import org.apache.rocketmq.dashboard.model.request.ConsumerConfigInfo;
import org.apache.rocketmq.dashboard.model.request.DeleteSubGroupRequest;
import org.apache.rocketmq.dashboard.model.request.ResetOffsetRequest;
import org.apache.rocketmq.dashboard.service.ClusterInfoService;
import org.apache.rocketmq.dashboard.service.impl.ConsumerServiceImpl;
import org.apache.rocketmq.dashboard.util.MockObjectUtil;
import org.apache.rocketmq.remoting.protocol.ResponseCode;
import org.apache.rocketmq.remoting.protocol.admin.ConsumeStats; import org.apache.rocketmq.remoting.protocol.admin.ConsumeStats;
import org.apache.rocketmq.remoting.protocol.admin.RollbackStats; import org.apache.rocketmq.remoting.protocol.admin.RollbackStats;
import org.apache.rocketmq.common.message.MessageQueue;
import org.apache.rocketmq.remoting.protocol.ResponseCode;
import org.apache.rocketmq.remoting.protocol.body.ClusterInfo; import org.apache.rocketmq.remoting.protocol.body.ClusterInfo;
import org.apache.rocketmq.remoting.protocol.body.Connection;
import org.apache.rocketmq.remoting.protocol.body.ConsumerConnection; import org.apache.rocketmq.remoting.protocol.body.ConsumerConnection;
import org.apache.rocketmq.remoting.protocol.body.ConsumerRunningInfo; import org.apache.rocketmq.remoting.protocol.body.ConsumerRunningInfo;
import org.apache.rocketmq.remoting.protocol.body.SubscriptionGroupWrapper; import org.apache.rocketmq.remoting.protocol.body.SubscriptionGroupWrapper;
import org.apache.rocketmq.remoting.protocol.heartbeat.ConsumeType; import org.apache.rocketmq.remoting.protocol.heartbeat.ConsumeType;
import org.apache.rocketmq.remoting.protocol.heartbeat.MessageModel; import org.apache.rocketmq.remoting.protocol.heartbeat.MessageModel;
import org.apache.rocketmq.remoting.protocol.subscription.SubscriptionGroupConfig; import org.apache.rocketmq.remoting.protocol.subscription.SubscriptionGroupConfig;
import org.apache.rocketmq.dashboard.model.request.ConsumerConfigInfo;
import org.apache.rocketmq.dashboard.model.request.DeleteSubGroupRequest;
import org.apache.rocketmq.dashboard.model.request.ResetOffsetRequest;
import org.apache.rocketmq.dashboard.service.impl.ConsumerServiceImpl;
import org.apache.rocketmq.dashboard.util.MockObjectUtil;
import org.apache.rocketmq.dashboard.model.TopicConsumerInfo;
import org.apache.rocketmq.dashboard.model.QueueStatInfo;
import org.apache.rocketmq.remoting.protocol.body.Connection;
import org.junit.Before; import org.junit.Before;
import org.junit.Test; import org.junit.Test;
import org.mockito.InjectMocks; import org.mockito.InjectMocks;
import org.mockito.Mock;
import org.mockito.Spy; import org.mockito.Spy;
import org.springframework.http.MediaType; import org.springframework.http.MediaType;
import org.springframework.test.web.servlet.request.MockMvcRequestBuilders; import org.springframework.test.web.servlet.request.MockMvcRequestBuilders;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import static org.hamcrest.Matchers.hasSize; import static org.hamcrest.Matchers.hasSize;
import static org.mockito.ArgumentMatchers.any; import static org.mockito.ArgumentMatchers.*;
import static org.mockito.ArgumentMatchers.anyBoolean;
import static org.mockito.ArgumentMatchers.anyLong;
import static org.mockito.ArgumentMatchers.anyString;
import static org.mockito.ArgumentMatchers.isNull;
import static org.mockito.Mockito.doNothing; import static org.mockito.Mockito.doNothing;
import static org.mockito.Mockito.doReturn; import static org.mockito.Mockito.doReturn;
import static org.mockito.Mockito.when; import static org.mockito.Mockito.when;
@@ -71,12 +70,18 @@ public class ConsumerControllerTest extends BaseControllerTest {
@Spy @Spy
private ConsumerServiceImpl consumerService; private ConsumerServiceImpl consumerService;
@Mock
private ClusterInfoService clusterInfoService;
@Before @Before
public void init() throws Exception { public void init() throws Exception {
// 2. mock ClusterInfo data
ClusterInfo mockClusterInfo = getClusterInfo();
when(clusterInfoService.get()).thenReturn(mockClusterInfo);
consumerService.afterPropertiesSet(); consumerService.afterPropertiesSet();
super.mockRmqConfigure(); super.mockRmqConfigure();
ClusterInfo clusterInfo = MockObjectUtil.createClusterInfo(); // ClusterInfo clusterInfo = MockObjectUtil.createClusterInfo();
when(mqAdminExt.examineBrokerClusterInfo()).thenReturn(clusterInfo); // when(mqAdminExt.examineBrokerClusterInfo()).thenReturn(clusterInfo);
SubscriptionGroupWrapper wrapper = MockObjectUtil.createSubscriptionGroupWrapper(); SubscriptionGroupWrapper wrapper = MockObjectUtil.createSubscriptionGroupWrapper();
when(mqAdminExt.getAllSubscriptionGroup(anyString(), anyLong())).thenReturn(wrapper); when(mqAdminExt.getAllSubscriptionGroup(anyString(), anyLong())).thenReturn(wrapper);
ConsumeStats stats = MockObjectUtil.createConsumeStats(); ConsumeStats stats = MockObjectUtil.createConsumeStats();
@@ -109,6 +114,7 @@ public class ConsumerControllerTest extends BaseControllerTest {
@Test @Test
public void testGroupQuery() throws Exception { public void testGroupQuery() throws Exception {
final String url = "/consumer/group.query"; final String url = "/consumer/group.query";
requestBuilder = MockMvcRequestBuilders.get(url); requestBuilder = MockMvcRequestBuilders.get(url);
requestBuilder.param("consumerGroup", "group_test"); requestBuilder.param("consumerGroup", "group_test");
perform = mockMvc.perform(requestBuilder); perform = mockMvc.perform(requestBuilder);
@@ -177,6 +183,10 @@ public class ConsumerControllerTest extends BaseControllerTest {
@Test @Test
public void testExamineSubscriptionGroupConfig() throws Exception { public void testExamineSubscriptionGroupConfig() throws Exception {
ClusterInfo mockClusterInfo = getClusterInfo();
{
when(clusterInfoService.get()).thenReturn(mockClusterInfo);
}
final String url = "/consumer/examineSubscriptionGroupConfig.query"; final String url = "/consumer/examineSubscriptionGroupConfig.query";
requestBuilder = MockMvcRequestBuilders.get(url); requestBuilder = MockMvcRequestBuilders.get(url);
requestBuilder.param("consumerGroup", "group_test"); requestBuilder.param("consumerGroup", "group_test");
@@ -188,7 +198,9 @@ public class ConsumerControllerTest extends BaseControllerTest {
@Test @Test
public void testDelete() throws Exception { public void testDelete() throws Exception {
final String url = "/consumer/deleteSubGroup.do"; final String url = "/consumer/deleteSubGroup.do";
ClusterInfo mockClusterInfo = getClusterInfo();
{ {
when(clusterInfoService.get()).thenReturn(mockClusterInfo);
doNothing().when(mqAdminExt).deleteSubscriptionGroup(any(), anyString()); doNothing().when(mqAdminExt).deleteSubscriptionGroup(any(), anyString());
doNothing().when(mqAdminExt).deleteTopicInBroker(any(), anyString()); doNothing().when(mqAdminExt).deleteTopicInBroker(any(), anyString());
doNothing().when(mqAdminExt).deleteTopicInNameServer(any(), anyString()); doNothing().when(mqAdminExt).deleteTopicInNameServer(any(), anyString());
@@ -214,7 +226,6 @@ public class ConsumerControllerTest extends BaseControllerTest {
requestBuilder.content(JSON.toJSONString(consumerConfigInfo)); requestBuilder.content(JSON.toJSONString(consumerConfigInfo));
perform = mockMvc.perform(requestBuilder); perform = mockMvc.perform(requestBuilder);
performErrorExpect(perform); performErrorExpect(perform);
{ {
doNothing().when(mqAdminExt).createAndUpdateSubscriptionGroupConfig(anyString(), any()); doNothing().when(mqAdminExt).createAndUpdateSubscriptionGroupConfig(anyString(), any());
} }
@@ -233,6 +244,7 @@ public class ConsumerControllerTest extends BaseControllerTest {
.andExpect(jsonPath("$.data").value(true)); .andExpect(jsonPath("$.data").value(true));
} }
@Test @Test
public void testQueryConsumerByTopic() throws Exception { public void testQueryConsumerByTopic() throws Exception {
// Prepare test data // Prepare test data
@@ -313,7 +325,8 @@ public class ConsumerControllerTest extends BaseControllerTest {
.andExpect(jsonPath("$.data.jstack").value("test")); .andExpect(jsonPath("$.data.jstack").value("test"));
} }
@Override protected Object getTestController() { @Override
protected Object getTestController() {
return consumerController; return consumerController;
} }
} }

View File

@@ -18,14 +18,6 @@ package org.apache.rocketmq.dashboard.controller;
import com.google.common.collect.Maps; import com.google.common.collect.Maps;
import com.google.common.io.Files; import com.google.common.io.Files;
import java.io.File;
import java.math.BigDecimal;
import java.text.DateFormat;
import java.text.SimpleDateFormat;
import java.util.ArrayList;
import java.util.Date;
import java.util.List;
import java.util.Map;
import org.apache.rocketmq.dashboard.service.impl.DashboardCollectServiceImpl; import org.apache.rocketmq.dashboard.service.impl.DashboardCollectServiceImpl;
import org.apache.rocketmq.dashboard.service.impl.DashboardServiceImpl; import org.apache.rocketmq.dashboard.service.impl.DashboardServiceImpl;
import org.apache.rocketmq.dashboard.util.JsonUtil; import org.apache.rocketmq.dashboard.util.JsonUtil;
@@ -36,6 +28,15 @@ import org.mockito.InjectMocks;
import org.mockito.Spy; import org.mockito.Spy;
import org.springframework.test.web.servlet.request.MockMvcRequestBuilders; import org.springframework.test.web.servlet.request.MockMvcRequestBuilders;
import java.io.File;
import java.math.BigDecimal;
import java.text.DateFormat;
import java.text.SimpleDateFormat;
import java.util.ArrayList;
import java.util.Date;
import java.util.List;
import java.util.Map;
import static org.hamcrest.Matchers.hasKey; import static org.hamcrest.Matchers.hasKey;
import static org.hamcrest.Matchers.hasSize; import static org.hamcrest.Matchers.hasSize;
import static org.mockito.Mockito.when; import static org.mockito.Mockito.when;

View File

@@ -18,13 +18,8 @@ package org.apache.rocketmq.dashboard.controller;
import com.alibaba.fastjson.JSON; import com.alibaba.fastjson.JSON;
import com.google.common.collect.Lists; import com.google.common.collect.Lists;
import java.util.List;
import org.apache.rocketmq.client.exception.MQClientException; import org.apache.rocketmq.client.exception.MQClientException;
import org.apache.rocketmq.common.MixAll; import org.apache.rocketmq.common.MixAll;
import org.apache.rocketmq.remoting.protocol.ResponseCode;
import org.apache.rocketmq.remoting.protocol.body.CMResult;
import org.apache.rocketmq.remoting.protocol.body.ConsumeMessageDirectlyResult;
import org.apache.rocketmq.remoting.protocol.route.TopicRouteData;
import org.apache.rocketmq.dashboard.model.DlqMessageRequest; import org.apache.rocketmq.dashboard.model.DlqMessageRequest;
import org.apache.rocketmq.dashboard.model.MessagePage; import org.apache.rocketmq.dashboard.model.MessagePage;
import org.apache.rocketmq.dashboard.model.MessageView; import org.apache.rocketmq.dashboard.model.MessageView;
@@ -32,6 +27,10 @@ import org.apache.rocketmq.dashboard.model.request.MessageQuery;
import org.apache.rocketmq.dashboard.service.impl.DlqMessageServiceImpl; import org.apache.rocketmq.dashboard.service.impl.DlqMessageServiceImpl;
import org.apache.rocketmq.dashboard.service.impl.MessageServiceImpl; import org.apache.rocketmq.dashboard.service.impl.MessageServiceImpl;
import org.apache.rocketmq.dashboard.util.MockObjectUtil; import org.apache.rocketmq.dashboard.util.MockObjectUtil;
import org.apache.rocketmq.remoting.protocol.ResponseCode;
import org.apache.rocketmq.remoting.protocol.body.CMResult;
import org.apache.rocketmq.remoting.protocol.body.ConsumeMessageDirectlyResult;
import org.apache.rocketmq.remoting.protocol.route.TopicRouteData;
import org.junit.Test; import org.junit.Test;
import org.mockito.InjectMocks; import org.mockito.InjectMocks;
import org.mockito.Mock; import org.mockito.Mock;
@@ -41,6 +40,8 @@ import org.springframework.data.domain.PageRequest;
import org.springframework.http.MediaType; import org.springframework.http.MediaType;
import org.springframework.test.web.servlet.request.MockMvcRequestBuilders; import org.springframework.test.web.servlet.request.MockMvcRequestBuilders;
import java.util.List;
import static org.hamcrest.Matchers.hasSize; import static org.hamcrest.Matchers.hasSize;
import static org.mockito.ArgumentMatchers.any; import static org.mockito.ArgumentMatchers.any;
import static org.mockito.Mockito.when; import static org.mockito.Mockito.when;
@@ -172,7 +173,8 @@ public class DlqMessageControllerTest extends BaseControllerTest {
.andExpect(content().contentType("application/vnd.ms-excel;charset=utf-8")); .andExpect(content().contentType("application/vnd.ms-excel;charset=utf-8"));
} }
@Override protected Object getTestController() { @Override
protected Object getTestController() {
return dlqMessageController; return dlqMessageController;
} }
} }

View File

@@ -16,20 +16,26 @@
*/ */
package org.apache.rocketmq.dashboard.controller; package org.apache.rocketmq.dashboard.controller;
import java.lang.reflect.Field;
import org.apache.rocketmq.dashboard.model.User; import org.apache.rocketmq.dashboard.model.User;
import org.apache.rocketmq.dashboard.service.impl.UserServiceImpl; import org.apache.rocketmq.dashboard.service.impl.UserServiceImpl;
import org.apache.rocketmq.dashboard.service.strategy.UserContext;
import org.apache.rocketmq.dashboard.service.strategy.UserStrategy;
import org.apache.rocketmq.dashboard.support.GlobalExceptionHandler;
import org.apache.rocketmq.dashboard.util.WebUtil; import org.apache.rocketmq.dashboard.util.WebUtil;
import org.junit.Assert;
import org.junit.Before; import org.junit.Before;
import org.junit.Test; import org.junit.Test;
import org.mockito.InjectMocks; import org.mockito.InjectMocks;
import org.mockito.Mock;
import org.mockito.MockitoAnnotations;
import org.mockito.Spy; import org.mockito.Spy;
import org.springframework.test.util.ReflectionTestUtils;
import org.springframework.util.ReflectionUtils;
import org.springframework.test.web.servlet.request.MockMvcRequestBuilders; import org.springframework.test.web.servlet.request.MockMvcRequestBuilders;
import org.springframework.test.web.servlet.setup.MockMvcBuilders;
import org.springframework.util.ReflectionUtils;
import java.lang.reflect.Field;
import static org.mockito.Mockito.when; import static org.mockito.Mockito.when;
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath; import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;
@@ -38,19 +44,27 @@ public class LoginControllerTest extends BaseControllerTest {
@InjectMocks @InjectMocks
private LoginController loginController; private LoginController loginController;
@Spy @Mock
private UserServiceImpl userService; private UserServiceImpl userService;
@Spy
private UserContext userContext;
@Spy
private UserStrategy userStrategy;
private String contextPath = "/rocketmq-console"; private String contextPath = "/rocketmq-console";
@Before @Before
public void init() { public void init() {
MockitoAnnotations.initMocks(this);
super.mockRmqConfigure(); super.mockRmqConfigure();
when(configure.isLoginRequired()).thenReturn(true); when(configure.isLoginRequired()).thenReturn(true);
when(configure.getRocketMqDashboardDataPath()).thenReturn(""); when(configure.getRocketMqDashboardDataPath()).thenReturn("");
Field contextPathField = ReflectionUtils.findField(LoginController.class, "contextPath"); Field contextPathField = ReflectionUtils.findField(LoginController.class, "contextPath");
ReflectionUtils.makeAccessible(contextPathField); ReflectionUtils.makeAccessible(contextPathField);
ReflectionUtils.setField(contextPathField, loginController, contextPath); ReflectionUtils.setField(contextPathField, loginController, contextPath);
mockMvc = MockMvcBuilders.standaloneSetup(loginController).setControllerAdvice(GlobalExceptionHandler.class).build();
} }
@Test @Test
@@ -60,57 +74,56 @@ public class LoginControllerTest extends BaseControllerTest {
requestBuilder.sessionAttr(WebUtil.USER_NAME, "admin"); requestBuilder.sessionAttr(WebUtil.USER_NAME, "admin");
perform = mockMvc.perform(requestBuilder); perform = mockMvc.perform(requestBuilder);
perform.andExpect(status().isOk()) perform.andExpect(status().isOk())
.andExpect(jsonPath("$.data.logined").value(true)) .andExpect(jsonPath("$.logined").value(true))
.andExpect(jsonPath("$.data.loginRequired").value(true)); .andExpect(jsonPath("$.loginRequired").value(true));
} }
@Test @Test
public void testLogin() throws Exception { public void testLogin() throws Exception {
final String url = "/login/login.do"; final String url = "/login/login.do";
final String username = "admin"; final String username = "admin";
final String rightPwd = "admin"; final String rightPwd = "admin";
final String wrongPwd = "rocketmq"; final String wrongPwd = "rocketmq";
{
UserServiceImpl.FileBasedUserInfoStore store // 模拟 userService.queryByName 方法返回一个用户
= new UserServiceImpl.FileBasedUserInfoStore(configure); User user = new User("admin", "admin", 1);
User user = store.queryByName(username); user.setPassword(rightPwd);
Assert.assertNotNull(user);
Assert.assertEquals(user.getPassword(), rightPwd);
ReflectionTestUtils.setField(userService, "fileBasedUserInfoStore", store);
}
// 1、login fail // 1、login fail
requestBuilder = MockMvcRequestBuilders.post(url); perform = mockMvc.perform(post(url)
requestBuilder.param("username", username) .param("username", username)
.param("password", wrongPwd); .param("password", wrongPwd));
perform = mockMvc.perform(requestBuilder);
perform.andExpect(status().isOk()) perform.andExpect(status().isOk())
.andExpect(jsonPath("$.data").doesNotExist()) .andExpect(jsonPath("$.data").doesNotExist())
.andExpect(jsonPath("$.status").value(-1)) .andExpect(jsonPath("$.status").value(-1))
.andExpect(jsonPath("$.errMsg").value("Bad username or password!")); .andExpect(jsonPath("$.errMsg").value("Bad username or password!"));
// 2、login success when(userService.queryByUsernameAndPassword(username, rightPwd)).thenReturn(user);
requestBuilder = MockMvcRequestBuilders.post(url);
requestBuilder.param("username", username) // 2、login success
.param("password", rightPwd); perform = mockMvc.perform(post(url)
perform = mockMvc.perform(requestBuilder); .param("username", username)
perform.andExpect(status().isOk()) .param("password", rightPwd));
.andExpect(jsonPath("$.data.contextPath").value(contextPath)); perform.andExpect(status().isOk())
.andExpect(jsonPath("$.contextPath").value(contextPath));
}
}
@Test @Test
public void testLogout() throws Exception { public void testLogout() throws Exception {
final String url = "/login/logout.do"; final String url = "/login/logout.do";
requestBuilder = MockMvcRequestBuilders.post(url); requestBuilder = post(url);
requestBuilder.sessionAttr(WebUtil.USER_NAME, "root"); requestBuilder.sessionAttr(WebUtil.USER_NAME, "root");
perform = mockMvc.perform(requestBuilder); perform = mockMvc.perform(requestBuilder);
perform.andExpect(status().isOk()) perform.andExpect(status().isOk())
.andExpect(jsonPath("$.data").value(contextPath)); .andExpect(jsonPath("$.data").value(contextPath));
} }
@Override protected Object getTestController() { @Override
protected Object getTestController() {
return loginController; return loginController;
} }
} }

View File

@@ -18,11 +18,6 @@ package org.apache.rocketmq.dashboard.controller;
import com.alibaba.fastjson.JSON; import com.alibaba.fastjson.JSON;
import com.google.common.cache.Cache; import com.google.common.cache.Cache;
import java.lang.reflect.Field;
import java.util.ArrayList;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
import org.apache.rocketmq.client.QueryResult; import org.apache.rocketmq.client.QueryResult;
import org.apache.rocketmq.client.consumer.DefaultMQPullConsumer; import org.apache.rocketmq.client.consumer.DefaultMQPullConsumer;
import org.apache.rocketmq.client.consumer.PullResult; import org.apache.rocketmq.client.consumer.PullResult;
@@ -32,28 +27,33 @@ import org.apache.rocketmq.client.exception.MQClientException;
import org.apache.rocketmq.common.message.MessageClientIDSetter; import org.apache.rocketmq.common.message.MessageClientIDSetter;
import org.apache.rocketmq.common.message.MessageExt; import org.apache.rocketmq.common.message.MessageExt;
import org.apache.rocketmq.common.message.MessageQueue; import org.apache.rocketmq.common.message.MessageQueue;
import org.apache.rocketmq.remoting.protocol.body.CMResult;
import org.apache.rocketmq.remoting.protocol.body.ConsumeMessageDirectlyResult;
import org.apache.rocketmq.remoting.protocol.body.ConsumerConnection;
import org.apache.rocketmq.dashboard.model.QueueOffsetInfo; import org.apache.rocketmq.dashboard.model.QueueOffsetInfo;
import org.apache.rocketmq.dashboard.model.request.MessageQuery; import org.apache.rocketmq.dashboard.model.request.MessageQuery;
import org.apache.rocketmq.dashboard.service.impl.MessageServiceImpl; import org.apache.rocketmq.dashboard.service.impl.MessageServiceImpl;
import org.apache.rocketmq.dashboard.support.AutoCloseConsumerWrapper;
import org.apache.rocketmq.dashboard.util.MockObjectUtil; import org.apache.rocketmq.dashboard.util.MockObjectUtil;
import org.apache.rocketmq.remoting.RPCHook;
import org.apache.rocketmq.remoting.protocol.body.CMResult;
import org.apache.rocketmq.remoting.protocol.body.ConsumeMessageDirectlyResult;
import org.apache.rocketmq.remoting.protocol.body.ConsumerConnection;
import org.apache.rocketmq.tools.admin.api.MessageTrack; import org.apache.rocketmq.tools.admin.api.MessageTrack;
import org.apache.rocketmq.tools.admin.api.TrackType; import org.apache.rocketmq.tools.admin.api.TrackType;
import org.junit.Before; import org.junit.Before;
import org.junit.Test; import org.junit.Test;
import org.mockito.InjectMocks; import org.mockito.InjectMocks;
import org.mockito.Mock;
import org.mockito.Spy; import org.mockito.Spy;
import org.springframework.http.MediaType; import org.springframework.http.MediaType;
import org.springframework.test.web.servlet.request.MockMvcRequestBuilders; import org.springframework.test.web.servlet.request.MockMvcRequestBuilders;
import java.lang.reflect.Field;
import java.util.ArrayList;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
import static org.hamcrest.Matchers.hasSize; import static org.hamcrest.Matchers.hasSize;
import static org.mockito.ArgumentMatchers.any; import static org.mockito.ArgumentMatchers.*;
import static org.mockito.ArgumentMatchers.anyBoolean;
import static org.mockito.ArgumentMatchers.anyInt;
import static org.mockito.ArgumentMatchers.anyLong;
import static org.mockito.ArgumentMatchers.anyString;
import static org.mockito.Mockito.mock; import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.when; import static org.mockito.Mockito.when;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath; import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath;
@@ -71,6 +71,9 @@ public class MessageControllerTest extends BaseControllerTest {
private DefaultMQPullConsumer defaultMQPullConsumer; private DefaultMQPullConsumer defaultMQPullConsumer;
@Mock
private AutoCloseConsumerWrapper autoCloseConsumerWrapper;
@Before @Before
public void init() throws Exception { public void init() throws Exception {
super.mockRmqConfigure(); super.mockRmqConfigure();
@@ -89,7 +92,6 @@ public class MessageControllerTest extends BaseControllerTest {
when(pullResult.getNextBeginOffset()).thenReturn(Long.MAX_VALUE); when(pullResult.getNextBeginOffset()).thenReturn(Long.MAX_VALUE);
when(pullResult.getPullStatus()).thenReturn(PullStatus.FOUND); when(pullResult.getPullStatus()).thenReturn(PullStatus.FOUND);
when(pullResult.getMsgFoundList()).thenReturn(wrappers); when(pullResult.getMsgFoundList()).thenReturn(wrappers);
when(messageService.buildDefaultMQPullConsumer(any(), anyBoolean())).thenReturn(defaultMQPullConsumer);
// Ensure searchOffset returns values that make sense for the test times // Ensure searchOffset returns values that make sense for the test times
when(defaultMQPullConsumer.searchOffset(any(MessageQueue.class), anyLong())).thenAnswer(invocation -> { when(defaultMQPullConsumer.searchOffset(any(MessageQueue.class), anyLong())).thenAnswer(invocation -> {
@@ -115,6 +117,7 @@ public class MessageControllerTest extends BaseControllerTest {
// Override the previous mock to ensure the test finds messages // Override the previous mock to ensure the test finds messages
when(defaultMQPullConsumer.pull(any(MessageQueue.class), anyString(), anyLong(), anyInt())) when(defaultMQPullConsumer.pull(any(MessageQueue.class), anyString(), anyLong(), anyInt()))
.thenReturn(pullResultWithMessages); .thenReturn(pullResultWithMessages);
when(autoCloseConsumerWrapper.getConsumer(any(RPCHook.class), anyBoolean())).thenReturn(defaultMQPullConsumer);
} }
} }
@@ -269,7 +272,8 @@ public class MessageControllerTest extends BaseControllerTest {
.andExpect(jsonPath("$.data.consumeResult").value(CMResult.CR_LATER.name())); .andExpect(jsonPath("$.data.consumeResult").value(CMResult.CR_LATER.name()));
} }
@Override protected Object getTestController() { @Override
protected Object getTestController() {
return messageController; return messageController;
} }
} }

View File

@@ -16,8 +16,6 @@
*/ */
package org.apache.rocketmq.dashboard.controller; package org.apache.rocketmq.dashboard.controller;
import java.util.ArrayList;
import java.util.List;
import org.apache.rocketmq.client.QueryResult; import org.apache.rocketmq.client.QueryResult;
import org.apache.rocketmq.client.exception.MQClientException; import org.apache.rocketmq.client.exception.MQClientException;
import org.apache.rocketmq.client.trace.TraceType; import org.apache.rocketmq.client.trace.TraceType;
@@ -33,6 +31,9 @@ import org.mockito.InjectMocks;
import org.mockito.Spy; import org.mockito.Spy;
import org.springframework.test.web.servlet.request.MockMvcRequestBuilders; import org.springframework.test.web.servlet.request.MockMvcRequestBuilders;
import java.util.ArrayList;
import java.util.List;
import static org.hamcrest.Matchers.hasSize; import static org.hamcrest.Matchers.hasSize;
import static org.mockito.ArgumentMatchers.any; import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.anyInt; import static org.mockito.ArgumentMatchers.anyInt;
@@ -135,7 +136,8 @@ public class MessageTraceControllerTest extends BaseControllerTest {
.andExpect(jsonPath("$.data.messageTraceViews", hasSize(4))); .andExpect(jsonPath("$.data.messageTraceViews", hasSize(4)));
} }
@Override protected Object getTestController() { @Override
protected Object getTestController() {
return messageTraceController; return messageTraceController;
} }
} }

View File

@@ -17,9 +17,6 @@
package org.apache.rocketmq.dashboard.controller; package org.apache.rocketmq.dashboard.controller;
import com.fasterxml.jackson.core.type.TypeReference; import com.fasterxml.jackson.core.type.TypeReference;
import java.io.File;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
import org.apache.rocketmq.common.MixAll; import org.apache.rocketmq.common.MixAll;
import org.apache.rocketmq.dashboard.model.ConsumerMonitorConfig; import org.apache.rocketmq.dashboard.model.ConsumerMonitorConfig;
import org.apache.rocketmq.dashboard.service.impl.MonitorServiceImpl; import org.apache.rocketmq.dashboard.service.impl.MonitorServiceImpl;
@@ -33,6 +30,10 @@ import org.mockito.Spy;
import org.springframework.test.util.ReflectionTestUtils; import org.springframework.test.util.ReflectionTestUtils;
import org.springframework.test.web.servlet.request.MockMvcRequestBuilders; import org.springframework.test.web.servlet.request.MockMvcRequestBuilders;
import java.io.File;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
import static org.mockito.Mockito.when; import static org.mockito.Mockito.when;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath; import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;
@@ -135,7 +136,8 @@ public class MonitorControllerTest extends BaseControllerTest {
} }
} }
@Override protected Object getTestController() { @Override
protected Object getTestController() {
return monitorController; return monitorController;
} }
} }

View File

@@ -44,7 +44,8 @@ public class NamesvrControllerTest extends BaseControllerTest {
Assert.assertEquals(namesrvAddr, "127.0.0.1:9876"); Assert.assertEquals(namesrvAddr, "127.0.0.1:9876");
} }
@Override protected Object getTestController() { @Override
protected Object getTestController() {
return namesvrController; return namesvrController;
} }
} }

Some files were not shown because too many files have changed in this diff Show More