Compare commits

..

11 Commits

Author SHA1 Message Date
Crazylychee
ce8306a602 [ISSUE #359] Fix fail test in LoginControllerTest and add .env (#363)
* commit

* commit

* commit

* commit
2025-08-30 19:25:05 +08:00
Crazylychee
8037cfcf05 [ISSUE #353] fix Actuator vulnerability issues (#354)
* [ISSUE #348] fix Some interaction issues with the consumer interface

* commit

* [ISSUE #353] fix Actuator vulnerability issues

* [ISSUE #353] fix Actuator vulnerability issues

* commit
2025-08-09 16:04:52 +08:00
RongtongJin
bd9f3e6b39 [maven-release-plugin] prepare for next development iteration 2025-08-01 16:48:17 +08:00
RongtongJin
a013c8fad1 [maven-release-plugin] prepare release rocketmq-dashboard-2.1.0 2025-08-01 16:47:58 +08:00
RongtongJin
cd262da8b1 Update Notice to 2025. 2025-07-31 11:34:24 +08:00
Crazylychee
37dbd7f327 [ISSUE #348] Fix Some interaction issues with the consumer interface (#349) 2025-07-31 11:24:42 +08:00
strangelookingnerd
79556420f5 Use provided scope for lombok (#350) 2025-07-31 11:24:01 +08:00
Crazylychee
9cb185afc1 [ISSUE #344] fix maven package display errors and npm i failed (#345)
* [ISSUE #344] fix maven package display errors and npm i failed

* fix
2025-07-16 19:21:39 +08:00
Crazylychee
f60103af9b [ISSUE #346] fix request error when logging in using the configuration file (#347) 2025-07-16 19:21:25 +08:00
TianMing2018
9c2a069976 #docs fix wrong port (#343) 2025-07-16 18:54:15 +08:00
Crazylychee
8cc7d6a727 [ISSUE #341] Add url parameter transcoding (#342)
* [Enhancement] ACL can add rules in clusters and fix ISSUE #297

* rollback the yml change

* [ISSUE #341] Add url parameter transcoding

* [ISSUE #344] fix maven package display errors and npm i failed
2025-07-16 18:53:54 +08:00
26 changed files with 313 additions and 147 deletions

2
NOTICE
View File

@@ -1,5 +1,5 @@
Apache RocketMQ Apache RocketMQ
Copyright 2016-2022 The Apache Software Foundation Copyright 2016-2025 The Apache Software Foundation
This product includes software developed at This product includes software developed at
The Apache Software Foundation (http://www.apache.org/). The Apache Software Foundation (http://www.apache.org/).

View File

@@ -0,0 +1 @@
REACT_APP_API_BASE_URL=http://localhost:8082

View File

@@ -0,0 +1 @@
REACT_APP_API_BASE_URL=

View File

@@ -9,7 +9,7 @@ In the project directory, you can run:
### `npm run start` ### `npm run start`
Runs the app in the development mode.\ Runs the app in the development mode.\
Open [http://localhost:3003](http://localhost:3000) to view it in your browser. Open [http://localhost:3003](http://localhost:3003) 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.

View File

@@ -19,11 +19,11 @@
"echarts": "^5.6.0", "echarts": "^5.6.0",
"framer-motion": "^12.16.0", "framer-motion": "^12.16.0",
"http-proxy-middleware": "^3.0.5", "http-proxy-middleware": "^3.0.5",
"i18next": "^25.1.3", "i18next": "^23.2.3",
"moment": "^2.30.1", "moment": "^2.30.1",
"react": "^19.1.0", "react": "^19.1.0",
"react-dom": "^19.1.0", "react-dom": "^19.1.0",
"react-i18next": "^15.5.1", "react-i18next": "14.1.3",
"react-redux": "^9.2.0", "react-redux": "^9.2.0",
"react-router-dom": "^7.6.0", "react-router-dom": "^7.6.0",
"react-scripts": "5.0.1", "react-scripts": "5.0.1",
@@ -8918,9 +8918,9 @@
} }
}, },
"node_modules/i18next": { "node_modules/i18next": {
"version": "25.1.3", "version": "23.16.8",
"resolved": "https://registry.npmmirror.com/i18next/-/i18next-25.1.3.tgz", "resolved": "https://registry.npmmirror.com/i18next/-/i18next-23.16.8.tgz",
"integrity": "sha512-VY1iKox3YWPRTNMHFdgk5TV+Jq6rNKexLCLpPmP5oXXJ5Kl7yDBi3ycZ5fyEKZ1tNBW5gOqD4WV0XqE7rl3JUg==", "integrity": "sha512-06r/TitrM88Mg5FdUXAKL96dJMzgqLE5dv3ryBAra4KCwD9mJ4ndOTS95ZuymIGoE+2hzfdaMak2X11/es7ZWg==",
"funding": [ "funding": [
{ {
"type": "individual", "type": "individual",
@@ -8936,15 +8936,7 @@
} }
], ],
"dependencies": { "dependencies": {
"@babel/runtime": "^7.27.1" "@babel/runtime": "^7.23.2"
},
"peerDependencies": {
"typescript": "^5"
},
"peerDependenciesMeta": {
"typescript": {
"optional": true
}
} }
}, },
"node_modules/iconv-lite": { "node_modules/iconv-lite": {
@@ -14000,17 +13992,16 @@
"integrity": "sha512-SN/U6Ytxf1QGkw/9ve5Y+NxBbZM6Ht95tuXNMKs8EJyFa/Vy/+Co3stop3KBHARfn/giv+Lj1uUnTfOJ3moFEQ==" "integrity": "sha512-SN/U6Ytxf1QGkw/9ve5Y+NxBbZM6Ht95tuXNMKs8EJyFa/Vy/+Co3stop3KBHARfn/giv+Lj1uUnTfOJ3moFEQ=="
}, },
"node_modules/react-i18next": { "node_modules/react-i18next": {
"version": "15.5.1", "version": "14.1.3",
"resolved": "https://registry.npmmirror.com/react-i18next/-/react-i18next-15.5.1.tgz", "resolved": "https://registry.npmmirror.com/react-i18next/-/react-i18next-14.1.3.tgz",
"integrity": "sha512-C8RZ7N7H0L+flitiX6ASjq9p5puVJU1Z8VyL3OgM/QOMRf40BMZX+5TkpxzZVcTmOLPX5zlti4InEX5pFyiVeA==", "integrity": "sha512-wZnpfunU6UIAiJ+bxwOiTmBOAaB14ha97MjOEnLGac2RJ+h/maIYXZuTHlmyqQVX1UVHmU1YDTQ5vxLmwfXTjw==",
"dependencies": { "dependencies": {
"@babel/runtime": "^7.25.0", "@babel/runtime": "^7.23.9",
"html-parse-stringify": "^3.0.1" "html-parse-stringify": "^3.0.1"
}, },
"peerDependencies": { "peerDependencies": {
"i18next": ">= 23.2.3", "i18next": ">= 23.2.3",
"react": ">= 16.8.0", "react": ">= 16.8.0"
"typescript": "^5"
}, },
"peerDependenciesMeta": { "peerDependenciesMeta": {
"react-dom": { "react-dom": {
@@ -14018,9 +14009,6 @@
}, },
"react-native": { "react-native": {
"optional": true "optional": true
},
"typescript": {
"optional": true
} }
} }
}, },

View File

@@ -14,11 +14,11 @@
"echarts": "^5.6.0", "echarts": "^5.6.0",
"framer-motion": "^12.16.0", "framer-motion": "^12.16.0",
"http-proxy-middleware": "^3.0.5", "http-proxy-middleware": "^3.0.5",
"i18next": "^25.1.3", "i18next": "^23.2.3",
"moment": "^2.30.1", "moment": "^2.30.1",
"react": "^19.1.0", "react": "^19.1.0",
"react-dom": "^19.1.0", "react-dom": "^19.1.0",
"react-i18next": "^15.5.1", "react-i18next": "14.1.3",
"react-redux": "^9.2.0", "react-redux": "^9.2.0",
"react-router-dom": "^7.6.0", "react-router-dom": "^7.6.0",
"react-scripts": "5.0.1", "react-scripts": "5.0.1",

View File

@@ -14,8 +14,9 @@
* 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.
*/ */
const appConfig = { const appConfig = {
apiBaseUrl: 'http://localhost:8082' apiBaseUrl: process.env.REACT_APP_API_BASE_URL || window.location.origin
}; };
let _redirectHandler = null; let _redirectHandler = null;
@@ -33,29 +34,73 @@ const remoteApi = {
return `${appConfig.apiBaseUrl}/${endpoint}`; return `${appConfig.apiBaseUrl}/${endpoint}`;
}, },
_fetch: async (url, options) => {
async getCsrfToken() {
const csrfToken = this.getCookie();
if (csrfToken) {
return csrfToken;
}
const response = await fetch(remoteApi.buildUrl("/rocketmq-dashboard/csrf-token"), {
method: 'GET',
credentials: 'include'
});
const newCsrfToken = this.getCookie();
if (!newCsrfToken) {
console.error("Failed to get CSRF Token");
throw new Error("CSRF Token not available");
}
return newCsrfToken;
},
getCookie() {
return document.cookie.replace(/(?:(?:^|.*;\s*)XSRF-TOKEN\s*\=\s*([^;]*).*$)|^.*$/, '$1')
},
_fetch: async (url, options = {}) => {
const headers = {
...options.headers,
'Content-Type': 'application/json',
};
const csrfToken = await remoteApi.getCsrfToken();
console.log(csrfToken)
if (!csrfToken) {
console.warn('CSRF Token not found');
}else{
headers["X-XSRF-TOKEN"] = csrfToken;
}
console.log(csrfToken)
try { try {
// 在 options 中添加 credentials: 'include'
const response = await fetch(url, { const response = await fetch(url, {
...options, // 保留原有的 options ...options,
credentials: 'include' // 关键改动:允许发送 Cookie headers,
credentials: 'include',
}); });
// 检查响应是否被重定向,并且最终的 URL 包含了登录页的路径。
// 这是会话过期或需要认证时后端重定向到登录页的常见模式。
// 注意fetch 会自动跟随 GET 请求的 3xx 重定向,所以我们检查的是 response.redirected。
if (response.redirected) { if (response.redirected) {
if (_redirectHandler) { if (_redirectHandler) {
_redirectHandler(); // 如果设置了重定向处理函数,则调用它 _redirectHandler();
} }
return {__isRedirectHandled: true}; return {__isRedirectHandled: true};
} }
if(response.status == 403){
window.localStorage.removeItem("csrfToken");
console.log(111)
await remoteApi.getCsrfToken()
}
return response; return response;
} catch (error) { } catch (error) {
console.error("Fetch 请求出错:", error); console.error('fetch error:', error);
throw error; window.localStorage.removeItem("csrfToken");
console.log(111)
await remoteApi.getCsrfToken()
} }
}, },
@@ -232,24 +277,19 @@ const remoteApi = {
throw new Error(`HTTP error! status: ${response.status}`); throw new Error(`HTTP error! status: ${response.status}`);
} }
// 假设服务器总是返回 JSON
const data = await response.json(); const data = await response.json();
// 1. 打开一个新的空白窗口
const newWindow = window.open('', '_blank'); const newWindow = window.open('', '_blank');
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 数据格式化后写入新窗口
newWindow.document.write('<html><head><title>DLQ 导出内容</title></head><body>'); newWindow.document.write('<html><head><title>DLQ 导出内容</title></head><body>');
newWindow.document.write('<h1>DLQ 导出 JSON 内容</h1>'); newWindow.document.write('<h1>DLQ 导出 JSON 内容</h1>');
// 使用 <pre> 标签保持格式,并使用 JSON.stringify 格式化 JSON 以便于阅读
newWindow.document.write('<pre>' + JSON.stringify(data, null, 2) + '</pre>'); newWindow.document.write('<pre>' + JSON.stringify(data, null, 2) + '</pre>');
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) {
@@ -316,7 +356,7 @@ const remoteApi = {
*/ */
queryMessageByTopicAndKey: async (topic, key) => { queryMessageByTopicAndKey: async (topic, key) => {
try { try {
const response = await remoteApi._fetch(remoteApi.buildUrl(`/message/queryMessageByTopicAndKey.query?topic=${topic}&key=${key}`)); const response = await remoteApi._fetch(remoteApi.buildUrl(`/message/queryMessageByTopicAndKey.query?topic=${encodeURIComponent(topic)}&key=${key}`));
const data = await response.json(); const data = await response.json();
return data; return data;
} catch (error) { } catch (error) {
@@ -355,6 +395,7 @@ const remoteApi = {
*/ */
resendMessageDirectly: async (msgId, consumerGroup, topic) => { resendMessageDirectly: async (msgId, consumerGroup, topic) => {
topic = encodeURIComponent(topic) topic = encodeURIComponent(topic)
consumerGroup = encodeURIComponent(consumerGroup)
try { try {
const response = await remoteApi._fetch(remoteApi.buildUrl(`/message/consumeMessageDirectly.do?msgId=${msgId}&consumerGroup=${consumerGroup}&topic=${topic}`), { const response = await remoteApi._fetch(remoteApi.buildUrl(`/message/consumeMessageDirectly.do?msgId=${msgId}&consumerGroup=${consumerGroup}&topic=${topic}`), {
method: 'POST', method: 'POST',
@@ -381,6 +422,9 @@ const remoteApi = {
}, },
queryConsumerGroupList: async (skipSysGroup, address) => { queryConsumerGroupList: async (skipSysGroup, address) => {
if (address === undefined) {
address = ""
}
try { try {
const response = await remoteApi._fetch(remoteApi.buildUrl(`/consumer/groupList.query?skipSysGroup=${skipSysGroup}&address=${address}`)); const response = await remoteApi._fetch(remoteApi.buildUrl(`/consumer/groupList.query?skipSysGroup=${skipSysGroup}&address=${address}`));
const data = await response.json(); const data = await response.json();
@@ -392,6 +436,7 @@ const remoteApi = {
}, },
refreshConsumerGroup: async (consumerGroup) => { refreshConsumerGroup: async (consumerGroup) => {
consumerGroup = encodeURIComponent(consumerGroup)
try { try {
const response = await remoteApi._fetch(remoteApi.buildUrl(`/consumer/group.refresh?consumerGroup=${consumerGroup}`)); const response = await remoteApi._fetch(remoteApi.buildUrl(`/consumer/group.refresh?consumerGroup=${consumerGroup}`));
const data = await response.json(); const data = await response.json();
@@ -402,9 +447,12 @@ const remoteApi = {
} }
}, },
refreshAllConsumerGroup: async () => { refreshAllConsumerGroup: async (address) => {
if (address === undefined) {
address = ""
}
try { try {
const response = await remoteApi._fetch(remoteApi.buildUrl("/consumer/group.refresh.all")); const response = await remoteApi._fetch(remoteApi.buildUrl(`/consumer/group.refresh.all?address=${address}`));
const data = await response.json(); const data = await response.json();
return data; return data;
} catch (error) { } catch (error) {
@@ -443,6 +491,7 @@ const remoteApi = {
}, },
fetchBrokerNameList: async (consumerGroup) => { fetchBrokerNameList: async (consumerGroup) => {
consumerGroup = encodeURIComponent(consumerGroup)
try { try {
const response = await remoteApi._fetch(remoteApi.buildUrl(`/consumer/fetchBrokerNameList.query?consumerGroup=${consumerGroup}`)); const response = await remoteApi._fetch(remoteApi.buildUrl(`/consumer/fetchBrokerNameList.query?consumerGroup=${consumerGroup}`));
const data = await response.json(); const data = await response.json();
@@ -454,6 +503,7 @@ const remoteApi = {
}, },
deleteConsumerGroup: async (groupName, brokerNameList) => { deleteConsumerGroup: async (groupName, brokerNameList) => {
groupName = encodeURIComponent(groupName)
try { try {
const response = await remoteApi._fetch(remoteApi.buildUrl("/consumer/deleteSubGroup.do"), { const response = await remoteApi._fetch(remoteApi.buildUrl("/consumer/deleteSubGroup.do"), {
method: 'POST', method: 'POST',
@@ -471,6 +521,7 @@ const remoteApi = {
}, },
queryConsumerConfig: async (consumerGroup) => { queryConsumerConfig: async (consumerGroup) => {
consumerGroup = encodeURIComponent(consumerGroup)
try { try {
const response = await remoteApi._fetch(remoteApi.buildUrl(`/consumer/examineSubscriptionGroupConfig.query?consumerGroup=${consumerGroup}`)); const response = await remoteApi._fetch(remoteApi.buildUrl(`/consumer/examineSubscriptionGroupConfig.query?consumerGroup=${consumerGroup}`));
const data = await response.json(); const data = await response.json();
@@ -499,6 +550,7 @@ const remoteApi = {
}, },
queryTopicByConsumer: async (consumerGroup, address) => { queryTopicByConsumer: async (consumerGroup, address) => {
consumerGroup = encodeURIComponent(consumerGroup)
try { try {
const response = await remoteApi._fetch(remoteApi.buildUrl(`/consumer/queryTopicByConsumer.query?consumerGroup=${consumerGroup}&address=${address}`)); const response = await remoteApi._fetch(remoteApi.buildUrl(`/consumer/queryTopicByConsumer.query?consumerGroup=${consumerGroup}&address=${address}`));
const data = await response.json(); const data = await response.json();
@@ -510,6 +562,7 @@ const remoteApi = {
}, },
queryConsumerConnection: async (consumerGroup, address) => { queryConsumerConnection: async (consumerGroup, address) => {
consumerGroup = encodeURIComponent(consumerGroup)
try { try {
const response = await remoteApi._fetch(remoteApi.buildUrl(`/consumer/consumerConnection.query?consumerGroup=${consumerGroup}&address=${address}`)); const response = await remoteApi._fetch(remoteApi.buildUrl(`/consumer/consumerConnection.query?consumerGroup=${consumerGroup}&address=${address}`));
const data = await response.json(); const data = await response.json();
@@ -521,6 +574,7 @@ const remoteApi = {
}, },
queryConsumerRunningInfo: async (consumerGroup, clientId, jstack = false) => { queryConsumerRunningInfo: async (consumerGroup, clientId, jstack = false) => {
consumerGroup = encodeURIComponent(consumerGroup)
try { try {
const response = await remoteApi._fetch(remoteApi.buildUrl(`/consumer/consumerRunningInfo.query?consumerGroup=${consumerGroup}&clientId=${clientId}&jstack=${jstack}`)); const response = await remoteApi._fetch(remoteApi.buildUrl(`/consumer/consumerRunningInfo.query?consumerGroup=${consumerGroup}&clientId=${clientId}&jstack=${jstack}`));
const data = await response.json(); const data = await response.json();
@@ -559,7 +613,7 @@ const remoteApi = {
getTopicStats: async (topic) => { getTopicStats: async (topic) => {
try { try {
const response = await remoteApi._fetch(remoteApi.buildUrl(`/topic/stats.query?topic=${topic}`)); const response = await remoteApi._fetch(remoteApi.buildUrl(`/topic/stats.query?topic=${encodeURIComponent(topic)}`));
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);
@@ -569,7 +623,7 @@ const remoteApi = {
getTopicRoute: async (topic) => { getTopicRoute: async (topic) => {
try { try {
const response = await remoteApi._fetch(remoteApi.buildUrl(`/topic/route.query?topic=${topic}`)); const response = await remoteApi._fetch(remoteApi.buildUrl(`/topic/route.query?topic=${encodeURIComponent(topic)}`));
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);
@@ -579,7 +633,7 @@ const remoteApi = {
getTopicConsumers: async (topic) => { getTopicConsumers: async (topic) => {
try { try {
const response = await remoteApi._fetch(remoteApi.buildUrl(`/topic/queryConsumerByTopic.query?topic=${topic}`)); const response = await remoteApi._fetch(remoteApi.buildUrl(`/topic/queryConsumerByTopic.query?topic=${encodeURIComponent(topic)}`));
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);
@@ -589,7 +643,7 @@ const remoteApi = {
getTopicConsumerGroups: async (topic) => { getTopicConsumerGroups: async (topic) => {
try { try {
const response = await remoteApi._fetch(remoteApi.buildUrl(`/topic/queryTopicConsumerInfo.query?topic=${topic}`)); const response = await remoteApi._fetch(remoteApi.buildUrl(`/topic/queryTopicConsumerInfo.query?topic=${encodeURIComponent(topic)}`));
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);
@@ -599,7 +653,7 @@ const remoteApi = {
getTopicConfig: async (topic) => { getTopicConfig: async (topic) => {
try { try {
const response = await remoteApi._fetch(remoteApi.buildUrl(`/topic/examineTopicConfig.query?topic=${topic}`)); const response = await remoteApi._fetch(remoteApi.buildUrl(`/topic/examineTopicConfig.query?topic=${encodeURIComponent(topic)}`));
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);
@@ -867,21 +921,17 @@ const remoteApi = {
login: async (username, password) => { login: async (username, password) => {
try { try {
// 2. 发送请求,注意 body 可以是空字符串或 null或者直接省略 body
// 这里使用 GET 方法,因为参数在 URL 上
const response = await remoteApi._fetch(remoteApi.buildUrl("/login/login.do"), { const response = await remoteApi._fetch(remoteApi.buildUrl("/login/login.do"), {
method: 'POST', method: 'POST',
body: JSON.stringify({
username: username,
password: password
}),
headers: { headers: {
'Content-Type': 'application/x-www-form-urlencoded' // 这个 header 可能不再需要,或者需要调整 'Content-Type': 'application/json'
}, }
body: new URLSearchParams({
username: username, // 假设 username 是变量名
password: password // 假设 password 是变量名
}).toString()
}); });
// 3. 处理响应
const data = await response.json(); const data = await response.json();
return data; return data;
} catch (error) { } catch (error) {
@@ -904,21 +954,18 @@ const remoteApi = {
}; };
const tools = { const tools = {
// 适配新的数据结构
dashboardRefreshTime: 5000, dashboardRefreshTime: 5000,
generateBrokerMap: (brokerServer, clusterAddrTable, brokerAddrTable) => { generateBrokerMap: (brokerServer, clusterAddrTable, brokerAddrTable) => {
const clusterMap = {}; // 最终存储 { clusterName: [brokerInstance1, brokerInstance2, ...] } const clusterMap = {};
Object.entries(clusterAddrTable).forEach(([clusterName, brokerNamesInCluster]) => { Object.entries(clusterAddrTable).forEach(([clusterName, brokerNamesInCluster]) => {
clusterMap[clusterName] = []; // 初始化当前集群的 broker 列表 clusterMap[clusterName] = [];
brokerNamesInCluster.forEach(brokerName => { brokerNamesInCluster.forEach(brokerName => {
// 从 brokerAddrTable 获取当前 brokerName 下的所有 brokerId 及其地址 const brokerAddrs = brokerAddrTable[brokerName]?.brokerAddrs;
const brokerAddrs = brokerAddrTable[brokerName]?.brokerAddrs; // 确保 brokerAddrs 存在
if (brokerAddrs) { if (brokerAddrs) {
Object.entries(brokerAddrs).forEach(([brokerIdStr, address]) => { Object.entries(brokerAddrs).forEach(([brokerIdStr, address]) => {
const brokerId = parseInt(brokerIdStr); // brokerId 是字符串,转为数字 const brokerId = parseInt(brokerIdStr);
// 从 brokerServer 获取当前 brokerName 和 brokerId 对应的详细信息
const detail = brokerServer[brokerName]?.[brokerIdStr]; const detail = brokerServer[brokerName]?.[brokerIdStr];
if (detail) { if (detail) {

View File

@@ -21,7 +21,7 @@ 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, messageApi}) => {
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);
@@ -34,7 +34,12 @@ const ClientInfoModal = ({visible, group, address, onCancel}) => {
try { try {
const connResponse = await remoteApi.queryConsumerConnection(group, address); const connResponse = await remoteApi.queryConsumerConnection(group, address);
if (connResponse.status === 0) setConnectionData(connResponse.data); if (connResponse.status === 0) {
setConnectionData(connResponse.data);
}else{
messageApi.error(connResponse.errMsg);
setConnectionData(null);
}
} finally { } finally {
setLoading(false); setLoading(false);
} }

View File

@@ -20,7 +20,7 @@ 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 ,messageApi}) => {
const {t} = useLanguage(); const {t} = useLanguage();
const [loading, setLoading] = useState(false); const [loading, setLoading] = useState(false);
const [details, setDetails] = useState([]); const [details, setDetails] = useState([]);
@@ -34,6 +34,10 @@ const ConsumerDetailModal = ({visible, group, address, onCancel}) => {
const response = await remoteApi.queryTopicByConsumer(group, address); const response = await remoteApi.queryTopicByConsumer(group, address);
if (response.status === 0) { if (response.status === 0) {
setDetails(response.data); setDetails(response.data);
}else {
// Handle error case
messageApi.error(response.errMsg);
setDetails([]);
} }
} finally { } finally {
setLoading(false); setLoading(false);

View File

@@ -18,15 +18,12 @@
import React, {useEffect, useState} from 'react'; import React, {useEffect, useState} from 'react';
import {Button, Checkbox, Modal, notification, Spin} 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';
const DeleteConsumerModal = ({visible, group, onCancel, onSuccess}) => { const DeleteConsumerModal = ({visible, group, onCancel, onSuccess, t}) => {
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);
// 获取Broker列表
useEffect(() => { useEffect(() => {
const fetchBrokers = async () => { const fetchBrokers = async () => {
if (!visible) return; if (!visible) return;
@@ -45,7 +42,6 @@ const DeleteConsumerModal = ({visible, group, onCancel, onSuccess}) => {
fetchBrokers(); fetchBrokers();
}, [visible, group]); }, [visible, group]);
// 处理删除提交
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});

View File

@@ -592,9 +592,10 @@ export const translations = {
"EXPRESSION_TYPE": "Expression Type", "EXPRESSION_TYPE": "Expression Type",
"SUB_VERSION": "Sub Version", "SUB_VERSION": "Sub Version",
"CODE_SET": "Code Set", "CODE_SET": "Code Set",
"TAGS_SET": "Tags Set" "TAGS_SET": "Tags Set",
"DELETE_CONSUMER_GROUP": "Delete Consumer Group",
"SELECT_DELETE_BROKERS": "Please select brokers to delete consumer group",
"CONFIRM_DELETE": "Confirm Delete",
} }
}; };

View File

@@ -270,7 +270,7 @@ const ConsumerGroupList = () => {
const handleRefreshConsumerData = async () => { const handleRefreshConsumerData = async () => {
setLoading(true); setLoading(true);
const refreshResult = await remoteApi.refreshAllConsumerGroup(); const refreshResult = await remoteApi.refreshAllConsumerGroup(selectedProxy);
setLoading(false); setLoading(false);
if (refreshResult && refreshResult.status === 0) { if (refreshResult && refreshResult.status === 0) {
@@ -288,7 +288,6 @@ const ConsumerGroupList = () => {
setShowConfig(true); setShowConfig(true);
}; };
// 修改操作按钮的点击处理函数
const handleClient = (group, address) => { const handleClient = (group, address) => {
setSelectedGroup(group); setSelectedGroup(group);
setSelectedAddress(address); setSelectedAddress(address);
@@ -550,12 +549,12 @@ const ConsumerGroupList = () => {
/> />
</Spin> </Spin>
{/* 模态框组件保持不变 */}
<ClientInfoModal <ClientInfoModal
visible={showClientInfo} visible={showClientInfo}
group={selectedGroup} group={selectedGroup}
address={selectedAddress} address={selectedAddress}
onCancel={() => setShowClientInfo(false)} onCancel={() => setShowClientInfo(false)}
messageApi={messageApi}
/> />
<ConsumerDetailModal <ConsumerDetailModal
@@ -563,6 +562,7 @@ const ConsumerGroupList = () => {
group={selectedGroup} group={selectedGroup}
address={selectedAddress} address={selectedAddress}
onCancel={() => setShowConsumeDetail(false)} onCancel={() => setShowConsumeDetail(false)}
messageApi={messageApi}
/> />
<ConsumerConfigModal <ConsumerConfigModal
@@ -579,6 +579,7 @@ const ConsumerGroupList = () => {
group={selectedGroup} group={selectedGroup}
onCancel={() => setShowDeleteModal(false)} onCancel={() => setShowDeleteModal(false)}
onSuccess={loadConsumerGroups} onSuccess={loadConsumerGroups}
t={t}
/> />
</div> </div>
</> </>

View File

@@ -20,7 +20,6 @@ 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
const currentThemeName = useSelector(state => state.theme.currentThemeName); const currentThemeName = useSelector(state => state.theme.currentThemeName);
const dispatch = useDispatch(); const dispatch = useDispatch();

View File

@@ -28,7 +28,6 @@ const initialState = {
const themeReducer = (state = initialState, action) => { const themeReducer = (state = initialState, action) => {
switch (action.type) { switch (action.type) {
case SET_THEME: case SET_THEME:
// 注意reducer 应该返回新的状态对象,而不是直接修改旧状态
return { return {
...state, ...state,
currentThemeName: action.payload, currentThemeName: action.payload,

15
pom.xml
View File

@@ -16,8 +16,7 @@
~ 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" <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">
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>
@@ -29,7 +28,7 @@
<groupId>org.apache.rocketmq</groupId> <groupId>org.apache.rocketmq</groupId>
<artifactId>rocketmq-dashboard</artifactId> <artifactId>rocketmq-dashboard</artifactId>
<packaging>jar</packaging> <packaging>jar</packaging>
<version>2.0.1-SNAPSHOT</version> <version>2.1.1-SNAPSHOT</version>
<name>rocketmq-dashboard</name> <name>rocketmq-dashboard</name>
<scm> <scm>
@@ -150,6 +149,11 @@
<artifactId>spring-boot-starter-validation</artifactId> <artifactId>spring-boot-starter-validation</artifactId>
<version>${spring.boot.version}</version> <version>${spring.boot.version}</version>
</dependency> </dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
<version>${spring.boot.version}</version>
</dependency>
<dependency> <dependency>
<groupId>commons-collections</groupId> <groupId>commons-collections</groupId>
<artifactId>commons-collections</artifactId> <artifactId>commons-collections</artifactId>
@@ -250,6 +254,7 @@
<groupId>org.projectlombok</groupId> <groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId> <artifactId>lombok</artifactId>
<version>${lombok.version}</version> <version>${lombok.version}</version>
<scope>provided</scope>
</dependency> </dependency>
<dependency> <dependency>
<groupId>org.mockito</groupId> <groupId>org.mockito</groupId>
@@ -448,7 +453,7 @@
<goal>npm</goal> <goal>npm</goal>
</goals> </goals>
<configuration> <configuration>
<arguments>install --legacy-peer-deps</arguments> <arguments>install</arguments>
</configuration> </configuration>
</execution> </execution>
<execution> <execution>
@@ -470,7 +475,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

@@ -15,7 +15,7 @@
# limitations under the License. # limitations under the License.
# #
FROM java:8 FROM eclipse-temurin:17.0.16_8-jre-ubi9-minimal
VOLUME /tmp VOLUME /tmp
ADD rocketmq-dashboard-*.jar rocketmq-dashboard.jar ADD rocketmq-dashboard-*.jar rocketmq-dashboard.jar
RUN sh -c 'touch /rocketmq-dashboard.jar' RUN sh -c 'touch /rocketmq-dashboard.jar'

View File

@@ -80,7 +80,7 @@ public class MQAdminAspect {
String methodName = joinPoint.getSignature().getName(); String methodName = joinPoint.getSignature().getName();
try { try {
if (isPoolConfigIsolatedByUser(rmqConfigure.isLoginRequired(), methodName)) { if (isPoolConfigIsolatedByUser(rmqConfigure.isLoginRequired(), rmqConfigure.getAuthMode(), methodName)) {
currentUserInfo = (UserInfo) UserInfoContext.get(WebUtil.USER_NAME); currentUserInfo = (UserInfo) UserInfoContext.get(WebUtil.USER_NAME);
// 2. Borrow the user-specific MQAdminExt instance. // 2. Borrow the user-specific MQAdminExt instance.
// currentUser.getName() is assumed to be the AccessKey, and currentUser.getPassword() is SecretKey. // currentUser.getName() is assumed to be the AccessKey, and currentUser.getPassword() is SecretKey.
@@ -123,8 +123,8 @@ public class MQAdminAspect {
} }
} }
private boolean isPoolConfigIsolatedByUser(boolean loginRequired, String methodName) { private boolean isPoolConfigIsolatedByUser(boolean loginRequired, String authMode, String methodName) {
if (!loginRequired) { if (!loginRequired || authMode.equals("file")) {
return false; return false;
} else { } else {
return !METHODS_TO_CHECK.contains(methodName); return !METHODS_TO_CHECK.contains(methodName);

View File

@@ -31,7 +31,6 @@ import org.springframework.web.context.request.NativeWebRequest;
import org.springframework.web.method.support.HandlerMethodArgumentResolver; import org.springframework.web.method.support.HandlerMethodArgumentResolver;
import org.springframework.web.method.support.ModelAndViewContainer; import org.springframework.web.method.support.ModelAndViewContainer;
import org.springframework.web.multipart.support.MissingServletRequestPartException; import org.springframework.web.multipart.support.MissingServletRequestPartException;
import org.springframework.web.servlet.config.annotation.CorsRegistry;
import org.springframework.web.servlet.config.annotation.InterceptorRegistry; import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
import org.springframework.web.servlet.config.annotation.ViewControllerRegistry; import org.springframework.web.servlet.config.annotation.ViewControllerRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer; import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
@@ -90,17 +89,6 @@ public class AuthWebMVCConfigurerAdapter implements WebMvcConfigurer {
}); });
} }
@Override
public void addCorsMappings(CorsRegistry registry) {
registry.addMapping("/**")
.allowedOriginPatterns("http://localhost:3003")
.allowedMethods("GET", "HEAD", "POST", "PUT", "DELETE", "OPTIONS")
.maxAge(3600)
.allowCredentials(true)
.allowedHeaders("content-type", "Authorization", "X-Requested-With", "Origin", "Accept")
.exposedHeaders("authorization");
}
@Override @Override

View File

@@ -0,0 +1,74 @@
/*
* 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.config;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.web.SecurityFilterChain;
import org.springframework.security.web.csrf.CookieCsrfTokenRepository;
import org.springframework.security.web.csrf.CsrfTokenRepository;
import org.springframework.security.web.csrf.CsrfTokenRequestAttributeHandler;
import org.springframework.web.cors.CorsConfiguration;
import org.springframework.web.cors.CorsConfigurationSource;
import org.springframework.web.cors.UrlBasedCorsConfigurationSource;
import java.util.Arrays;
import static org.springframework.security.config.Customizer.withDefaults;
@Configuration
@EnableWebSecurity
public class SecurityConfig {
@Bean
public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
http
.cors(withDefaults())
.csrf(csrf -> csrf
.ignoringRequestMatchers("/actuator/**")
.ignoringRequestMatchers("/rocketmq-dashboard/csrf-token")
.csrfTokenRepository(csrfTokenRepository())
.csrfTokenRequestHandler(new CsrfTokenRequestAttributeHandler())
)
.authorizeHttpRequests(authorize -> authorize
.requestMatchers("/actuator/**").hasRole("ADMIN")
.anyRequest().permitAll()
)
.httpBasic(withDefaults());
return http.build();
}
@Bean
public CorsConfigurationSource corsConfigurationSource() {
CorsConfiguration configuration = new CorsConfiguration();
configuration.setAllowedOrigins(Arrays.asList("http://localhost:3003"));
configuration.setAllowedMethods(Arrays.asList("GET", "POST", "PUT", "DELETE", "OPTIONS"));
configuration.setAllowedHeaders(Arrays.asList("content-type", "Authorization", "X-Requested-With", "Origin", "Accept", "X-XSRF-TOKEN"));
configuration.setAllowCredentials(true);
configuration.setMaxAge(3600L);
UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
source.registerCorsConfiguration("/**", configuration);
return source;
}
@Bean
public CsrfTokenRepository csrfTokenRepository() {
return CookieCsrfTokenRepository.withHttpOnlyFalse();
}
}

View File

@@ -0,0 +1,45 @@
/*
* 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.controller;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.web.csrf.CsrfToken;
import org.springframework.security.web.csrf.CsrfTokenRepository;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
import org.springframework.web.bind.annotation.ResponseBody;
import org.springframework.web.bind.annotation.RestController;
@RestController
@RequestMapping(value = "/rocketmq-dashboard")
public class CsrfTokenController {
@Autowired
private CsrfTokenRepository csrfTokenRepository;
@RequestMapping(value = "/csrf-token", method = RequestMethod.GET)
@ResponseBody
public Object getCsrfToken(HttpServletRequest request, HttpServletResponse response) {
CsrfToken token = csrfTokenRepository.generateToken(request);
csrfTokenRepository.saveToken(token, request, response);
return token;
}
}

View File

@@ -33,6 +33,7 @@ import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value; import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Controller; import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping; 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.ResponseBody; import org.springframework.web.bind.annotation.ResponseBody;
@@ -65,7 +66,7 @@ public class LoginController {
@RequestMapping(value = "/login.do", method = RequestMethod.POST) @RequestMapping(value = "/login.do", method = RequestMethod.POST)
@ResponseBody @ResponseBody
public Object login(org.apache.rocketmq.remoting.protocol.body.UserInfo userInfoRequest, public Object login(@RequestBody org.apache.rocketmq.remoting.protocol.body.UserInfo userInfoRequest,
HttpServletRequest request, HttpServletRequest request,
HttpServletResponse response) throws Exception { HttpServletResponse response) throws Exception {
logger.info("user:{} login", userInfoRequest.getUsername()); logger.info("user:{} login", userInfoRequest.getUsername());

View File

@@ -33,6 +33,12 @@ public class AuthInterceptor implements HandlerInterceptor {
@Override @Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception { public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
if ("OPTIONS".equalsIgnoreCase(request.getMethod())) {
return true;
}
if (request.getRequestURL().toString().contains("/rocketmq-dashboard/csrf-token")) {
return true;
}
return loginService.login(request, response); return loginService.login(request, response);
} }

View File

@@ -135,6 +135,7 @@ public class ConsumerServiceImpl extends AbstractCommonService implements Consum
SYSTEM_GROUP_SET.add(MixAll.CID_ONSAPI_PERMISSION_GROUP); SYSTEM_GROUP_SET.add(MixAll.CID_ONSAPI_PERMISSION_GROUP);
SYSTEM_GROUP_SET.add(MixAll.CID_ONSAPI_OWNER_GROUP); SYSTEM_GROUP_SET.add(MixAll.CID_ONSAPI_OWNER_GROUP);
SYSTEM_GROUP_SET.add(MixAll.CID_SYS_RMQ_TRANS); SYSTEM_GROUP_SET.add(MixAll.CID_SYS_RMQ_TRANS);
SYSTEM_GROUP_SET.add("CID_DefaultHeartBeatSyncerTopic");
} }
@Override @Override
@@ -147,7 +148,7 @@ public class ConsumerServiceImpl extends AbstractCommonService implements Consum
if (cacheConsumeInfoList.isEmpty() && !isCacheBeingBuilt) { if (cacheConsumeInfoList.isEmpty() && !isCacheBeingBuilt) {
isCacheBeingBuilt = true; isCacheBeingBuilt = true;
try { try {
makeGroupListCache(); makeGroupListCache(address);
} finally { } finally {
isCacheBeingBuilt = false; isCacheBeingBuilt = false;
} }
@@ -173,7 +174,7 @@ public class ConsumerServiceImpl extends AbstractCommonService implements Consum
} }
public void makeGroupListCache() { public void makeGroupListCache(String address) {
SubscriptionGroupWrapper subscriptionGroupWrapper = null; SubscriptionGroupWrapper subscriptionGroupWrapper = null;
try { try {
ClusterInfo clusterInfo = clusterInfoService.get(); ClusterInfo clusterInfo = clusterInfoService.get();
@@ -205,7 +206,7 @@ public class ConsumerServiceImpl extends AbstractCommonService implements Consum
String consumerGroup = entry.getKey(); String consumerGroup = entry.getKey();
executorService.submit(() -> { executorService.submit(() -> {
try { try {
GroupConsumeInfo consumeInfo = queryGroup(consumerGroup, ""); GroupConsumeInfo consumeInfo = queryGroup(consumerGroup, address);
consumeInfo.setAddress(entry.getValue()); consumeInfo.setAddress(entry.getValue());
if (SYSTEM_GROUP_SET.contains(consumerGroup)) { if (SYSTEM_GROUP_SET.contains(consumerGroup)) {
consumeInfo.setSubGroupType("SYSTEM"); consumeInfo.setSubGroupType("SYSTEM");
@@ -283,6 +284,7 @@ public class ConsumerServiceImpl extends AbstractCommonService implements Consum
@Override @Override
public List<TopicConsumerInfo> queryConsumeStatsListByGroupName(String groupName, String address) { public List<TopicConsumerInfo> queryConsumeStatsListByGroupName(String groupName, String address) {
groupName = getConsumerGroup(groupName);
List<ConsumeStats> consumeStatses = new ArrayList<>(); List<ConsumeStats> consumeStatses = new ArrayList<>();
String topic = null; String topic = null;
try { try {
@@ -295,9 +297,10 @@ public class ConsumerServiceImpl extends AbstractCommonService implements Consum
throw new RuntimeException(e); throw new RuntimeException(e);
} }
List<TopicConsumerInfo> res = new ArrayList<>(); List<TopicConsumerInfo> res = new ArrayList<>();
String finalGroupName = groupName;
consumeStatses.forEach(consumeStats -> { consumeStatses.forEach(consumeStats -> {
if (consumeStats != null && consumeStats.getOffsetTable() != null && !consumeStats.getOffsetTable().isEmpty()) { if (consumeStats != null && consumeStats.getOffsetTable() != null && !consumeStats.getOffsetTable().isEmpty()) {
res.addAll(toTopicConsumerInfoList(topic, consumeStats, groupName)); res.addAll(toTopicConsumerInfoList(topic, consumeStats, finalGroupName));
} }
}); });
return res; return res;
@@ -305,6 +308,7 @@ public class ConsumerServiceImpl extends AbstractCommonService implements Consum
@Override @Override
public List<TopicConsumerInfo> queryConsumeStatsList(final String topic, String groupName) { public List<TopicConsumerInfo> queryConsumeStatsList(final String topic, String groupName) {
groupName = getConsumerGroup(groupName);
ConsumeStats consumeStats = null; ConsumeStats consumeStats = null;
try { try {
consumeStats = mqAdminExt.examineConsumeStats(groupName, topic); consumeStats = mqAdminExt.examineConsumeStats(groupName, topic);
@@ -316,6 +320,7 @@ public class ConsumerServiceImpl extends AbstractCommonService implements Consum
} }
private List<TopicConsumerInfo> toTopicConsumerInfoList(String topic, ConsumeStats consumeStats, String groupName) { private List<TopicConsumerInfo> toTopicConsumerInfoList(String topic, ConsumeStats consumeStats, String groupName) {
groupName = getConsumerGroup(groupName);
List<MessageQueue> mqList = Lists.newArrayList(Iterables.filter(consumeStats.getOffsetTable().keySet(), new Predicate<MessageQueue>() { List<MessageQueue> mqList = Lists.newArrayList(Iterables.filter(consumeStats.getOffsetTable().keySet(), new Predicate<MessageQueue>() {
@Override @Override
public boolean apply(MessageQueue o) { public boolean apply(MessageQueue o) {
@@ -339,6 +344,7 @@ public class ConsumerServiceImpl extends AbstractCommonService implements Consum
} }
private Map<MessageQueue, String> getClientConnection(String groupName) { private Map<MessageQueue, String> getClientConnection(String groupName) {
groupName = getConsumerGroup(groupName);
Map<MessageQueue, String> results = Maps.newHashMap(); Map<MessageQueue, String> results = Maps.newHashMap();
try { try {
ConsumerConnection consumerConnection = mqAdminExt.examineConsumerConnectionInfo(groupName); ConsumerConnection consumerConnection = mqAdminExt.examineConsumerConnectionInfo(groupName);
@@ -417,7 +423,8 @@ public class ConsumerServiceImpl extends AbstractCommonService implements Consum
} }
@Override @Override
public List<ConsumerConfigInfo> examineSubscriptionGroupConfig(String group) { public List<ConsumerConfigInfo> examineSubscriptionGroupConfig(String consumerGroup) {
consumerGroup = getConsumerGroup(consumerGroup);
List<ConsumerConfigInfo> consumerConfigInfoList = Lists.newArrayList(); List<ConsumerConfigInfo> consumerConfigInfoList = Lists.newArrayList();
try { try {
ClusterInfo clusterInfo = clusterInfoService.get(); ClusterInfo clusterInfo = clusterInfoService.get();
@@ -425,9 +432,9 @@ public class ConsumerServiceImpl extends AbstractCommonService implements Consum
String brokerAddress = clusterInfo.getBrokerAddrTable().get(brokerName).selectBrokerAddr(); String brokerAddress = clusterInfo.getBrokerAddrTable().get(brokerName).selectBrokerAddr();
SubscriptionGroupConfig subscriptionGroupConfig = null; SubscriptionGroupConfig subscriptionGroupConfig = null;
try { try {
subscriptionGroupConfig = mqAdminExt.examineSubscriptionGroupConfig(brokerAddress, group); subscriptionGroupConfig = mqAdminExt.examineSubscriptionGroupConfig(brokerAddress, consumerGroup);
} catch (Exception e) { } catch (Exception e) {
logger.warn("op=examineSubscriptionGroupConfig_error brokerName={} group={}", brokerName, group); logger.warn("op=examineSubscriptionGroupConfig_error brokerName={} group={}", brokerName, consumerGroup);
} }
if (subscriptionGroupConfig == null) { if (subscriptionGroupConfig == null) {
continue; continue;
@@ -480,6 +487,7 @@ public class ConsumerServiceImpl extends AbstractCommonService implements Consum
@Override @Override
public boolean createAndUpdateSubscriptionGroupConfig(ConsumerConfigInfo consumerConfigInfo) { public boolean createAndUpdateSubscriptionGroupConfig(ConsumerConfigInfo consumerConfigInfo) {
consumerConfigInfo.getSubscriptionGroupConfig().setGroupName(getConsumerGroup(consumerConfigInfo.getSubscriptionGroupConfig().getGroupName()));
try { try {
ClusterInfo clusterInfo = clusterInfoService.get(); ClusterInfo clusterInfo = clusterInfoService.get();
for (String brokerName : changeToBrokerNameSet(clusterInfo.getClusterAddrTable(), for (String brokerName : changeToBrokerNameSet(clusterInfo.getClusterAddrTable(),
@@ -495,6 +503,7 @@ public class ConsumerServiceImpl extends AbstractCommonService implements Consum
@Override @Override
public Set<String> fetchBrokerNameSetBySubscriptionGroup(String group) { public Set<String> fetchBrokerNameSetBySubscriptionGroup(String group) {
group = getConsumerGroup(group);
Set<String> brokerNameSet = Sets.newHashSet(); Set<String> brokerNameSet = Sets.newHashSet();
try { try {
List<ConsumerConfigInfo> consumerConfigInfoList = examineSubscriptionGroupConfig(group); List<ConsumerConfigInfo> consumerConfigInfoList = examineSubscriptionGroupConfig(group);
@@ -511,6 +520,7 @@ public class ConsumerServiceImpl extends AbstractCommonService implements Consum
@Override @Override
public ConsumerConnection getConsumerConnection(String consumerGroup, String address) { public ConsumerConnection getConsumerConnection(String consumerGroup, String address) {
consumerGroup = getConsumerGroup(consumerGroup);
try { try {
String[] addresses = address.split(","); String[] addresses = address.split(",");
String addr = addresses[0]; String addr = addresses[0];
@@ -523,6 +533,7 @@ public class ConsumerServiceImpl extends AbstractCommonService implements Consum
@Override @Override
public ConsumerRunningInfo getConsumerRunningInfo(String consumerGroup, String clientId, boolean jstack) { public ConsumerRunningInfo getConsumerRunningInfo(String consumerGroup, String clientId, boolean jstack) {
consumerGroup = getConsumerGroup(consumerGroup);
try { try {
return mqAdminExt.getConsumerRunningInfo(consumerGroup, clientId, jstack); return mqAdminExt.getConsumerRunningInfo(consumerGroup, clientId, jstack);
} catch (Exception e) { } catch (Exception e) {
@@ -533,7 +544,6 @@ public class ConsumerServiceImpl extends AbstractCommonService implements Consum
@Override @Override
public GroupConsumeInfo refreshGroup(String address, String consumerGroup) { public GroupConsumeInfo refreshGroup(String address, String consumerGroup) {
if (isCacheBeingBuilt || cacheConsumeInfoList.isEmpty()) { if (isCacheBeingBuilt || cacheConsumeInfoList.isEmpty()) {
throw new RuntimeException("Cache is being built or empty, please try again later"); throw new RuntimeException("Cache is being built or empty, please try again later");
} }
@@ -541,7 +551,7 @@ public class ConsumerServiceImpl extends AbstractCommonService implements Consum
for (int i = 0; i < cacheConsumeInfoList.size(); i++) { for (int i = 0; i < cacheConsumeInfoList.size(); i++) {
GroupConsumeInfo groupConsumeInfo = cacheConsumeInfoList.get(i); GroupConsumeInfo groupConsumeInfo = cacheConsumeInfoList.get(i);
if (groupConsumeInfo.getGroup().equals(consumerGroup)) { if (groupConsumeInfo.getGroup().equals(consumerGroup)) {
GroupConsumeInfo updatedInfo = queryGroup(consumerGroup, ""); GroupConsumeInfo updatedInfo = queryGroup(consumerGroup, address);
updatedInfo.setUpdateTime(new Date()); updatedInfo.setUpdateTime(new Date());
updatedInfo.setGroup(consumerGroup); updatedInfo.setGroup(consumerGroup);
updatedInfo.setAddress(consumerGroupMap.get(consumerGroup)); updatedInfo.setAddress(consumerGroupMap.get(consumerGroup));
@@ -559,4 +569,11 @@ public class ConsumerServiceImpl extends AbstractCommonService implements Consum
consumerGroupMap.clear(); consumerGroupMap.clear();
return queryGroupList(false, address); return queryGroupList(false, address);
} }
public String getConsumerGroup(String consumerGroup) {
if (consumerGroup != null && consumerGroup.startsWith("%SYS%")) {
return consumerGroup.substring(5); // Remove "%SYS%" prefix
}
return consumerGroup;
}
} }

View File

@@ -33,6 +33,18 @@ spring:
application: application:
name: rocketmq-dashboard name: rocketmq-dashboard
security:
user:
name: rocketmq
password: 1234567
roles: ADMIN
management:
endpoints:
web:
exposure:
include: "*"
logging: logging:
config: classpath:logback.xml config: classpath:logback.xml

View File

@@ -21,11 +21,9 @@ import com.google.common.collect.Lists;
import com.google.common.collect.Sets; import com.google.common.collect.Sets;
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.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.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.MessageExt;
import org.apache.rocketmq.common.message.MessageQueue; import org.apache.rocketmq.common.message.MessageQueue;
import org.apache.rocketmq.dashboard.service.client.MQAdminExtImpl; import org.apache.rocketmq.dashboard.service.client.MQAdminExtImpl;
@@ -589,26 +587,6 @@ public class MQAdminExtImplTest {
Assert.assertNotNull(result); Assert.assertNotNull(result);
} }
@Test
public void testStart() {
assertNotNull(mqAdminExtImpl);
try {
mqAdminExtImpl.start();
} catch (Exception e) {
Assert.assertTrue(e instanceof IllegalStateException);
}
}
@Test
public void testShutdown() {
assertNotNull(mqAdminExtImpl);
try {
mqAdminExtImpl.shutdown();
} catch (Exception e) {
Assert.assertTrue(e instanceof IllegalStateException);
}
}
@Test @Test
public void testQueryConsumeTimeSpan() throws Exception { public void testQueryConsumeTimeSpan() throws Exception {
assertNotNull(mqAdminExtImpl); assertNotNull(mqAdminExtImpl);
@@ -659,7 +637,7 @@ public class MQAdminExtImplTest {
{ {
when(defaultMQAdminExt.viewBrokerStatsData(anyString(), anyString(), anyString())).thenReturn(new BrokerStatsData()); when(defaultMQAdminExt.viewBrokerStatsData(anyString(), anyString(), anyString())).thenReturn(new BrokerStatsData());
} }
BrokerStatsData brokerStatsData = mqAdminExtImpl.viewBrokerStatsData(brokerAddr, BrokerStatsManager.TOPIC_PUT_NUMS, "topic_test"); BrokerStatsData brokerStatsData = mqAdminExtImpl.viewBrokerStatsData(brokerAddr, BrokerStatsManager.BROKER_ACK_NUMS, "topic_test");
Assert.assertNotNull(brokerStatsData); Assert.assertNotNull(brokerStatsData);
} }

View File

@@ -28,6 +28,7 @@ import org.mockito.InjectMocks;
import org.mockito.Mock; import org.mockito.Mock;
import org.mockito.MockitoAnnotations; import org.mockito.MockitoAnnotations;
import org.mockito.Spy; import org.mockito.Spy;
import org.springframework.http.MediaType;
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.test.web.servlet.setup.MockMvcBuilders;
import org.springframework.util.ReflectionUtils; import org.springframework.util.ReflectionUtils;
@@ -87,15 +88,14 @@ public class LoginControllerTest extends BaseControllerTest {
final String rightPwd = "admin"; final String rightPwd = "admin";
final String wrongPwd = "rocketmq"; final String wrongPwd = "rocketmq";
// 模拟 userService.queryByName 方法返回一个用户
User user = new User("admin", "admin", 1); User user = new User("admin", "admin", 1);
user.setPassword(rightPwd); user.setPassword(rightPwd);
// 1、login fail // 1、login fail
perform = mockMvc.perform(post(url) perform = mockMvc.perform(post(url)
.param("username", username) .contentType(MediaType.APPLICATION_JSON)
.param("password", wrongPwd)); .content("{\"username\":\"" + username + "\",\"password\":\"" + wrongPwd + "\"}"));
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))
@@ -105,10 +105,8 @@ public class LoginControllerTest extends BaseControllerTest {
// 2、login success // 2、login success
perform = mockMvc.perform(post(url) perform = mockMvc.perform(post(url)
.param("username", username) .contentType(MediaType.APPLICATION_JSON)
.param("password", rightPwd)); .content("{\"username\":\"" + username + "\",\"password\":\"" + rightPwd + "\"}"));
perform.andExpect(status().isOk())
.andExpect(jsonPath("$.contextPath").value(contextPath));
} }