mirror of
https://github.com/apache/rocketmq-dashboard.git
synced 2025-09-10 11:40:01 +08:00
Compare commits
17 Commits
refactor
...
a5138eb0d8
Author | SHA1 | Date | |
---|---|---|---|
|
a5138eb0d8 | ||
|
b43c7abe52 | ||
|
bfd0e26737 | ||
|
31d8086db3 | ||
|
8564296440 | ||
|
e81dceb6ae | ||
|
bc1a05d16c | ||
|
3cbff604e6 | ||
|
bd94e8c4f5 | ||
|
52545ccd23 | ||
|
b75ace4804 | ||
|
eb51da6ca4 | ||
|
c85aa2e2a9 | ||
|
a450594ace | ||
|
bbabd1cd0d | ||
|
e76185437f | ||
|
3d13e4e2b8 |
3
.gitignore
vendored
3
.gitignore
vendored
@@ -5,4 +5,5 @@
|
||||
.project
|
||||
.factorypath
|
||||
.settings/
|
||||
.vscode
|
||||
.vscode
|
||||
htmlReport/
|
70
frontend-new/README.md
Normal file
70
frontend-new/README.md
Normal file
@@ -0,0 +1,70 @@
|
||||
# Getting Started with Create React App
|
||||
|
||||
This project was bootstrapped with [Create React App](https://github.com/facebook/create-react-app).
|
||||
|
||||
## Available Scripts
|
||||
|
||||
In the project directory, you can run:
|
||||
|
||||
### `npm start`
|
||||
|
||||
Runs the app in the development mode.\
|
||||
Open [http://localhost:3000](http://localhost:3000) to view it in your browser.
|
||||
|
||||
The page will reload when you make changes.\
|
||||
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`
|
||||
|
||||
Builds the app for production to the `build` folder.\
|
||||
It correctly bundles React in production mode and optimizes the build for the best performance.
|
||||
|
||||
The build is minified and the filenames include the hashes.\
|
||||
Your app is ready to be deployed!
|
||||
|
||||
See the section about [deployment](https://facebook.github.io/create-react-app/docs/deployment) for more information.
|
||||
|
||||
### `npm run eject`
|
||||
|
||||
**Note: this is a one-way operation. Once you `eject`, you can't go back!**
|
||||
|
||||
If you aren't satisfied with the build tool and configuration choices, you can `eject` at any time. This command will remove the single build dependency from your project.
|
||||
|
||||
Instead, it will copy all the configuration files and the transitive dependencies (webpack, Babel, ESLint, etc) right into your project so you have full control over them. All of the commands except `eject` will still work, but they will point to the copied scripts so you can tweak them. At this point you're on your own.
|
||||
|
||||
You don't have to ever use `eject`. The curated feature set is suitable for small and middle deployments, and you shouldn't feel obligated to use this feature. However we understand that this tool wouldn't be useful if you couldn't customize it when you are ready for it.
|
||||
|
||||
## Learn More
|
||||
|
||||
You can learn more in the [Create React App documentation](https://facebook.github.io/create-react-app/docs/getting-started).
|
||||
|
||||
To learn React, check out the [React documentation](https://reactjs.org/).
|
||||
|
||||
### Code Splitting
|
||||
|
||||
This section has moved here: [https://facebook.github.io/create-react-app/docs/code-splitting](https://facebook.github.io/create-react-app/docs/code-splitting)
|
||||
|
||||
### Analyzing the Bundle Size
|
||||
|
||||
This section has moved here: [https://facebook.github.io/create-react-app/docs/analyzing-the-bundle-size](https://facebook.github.io/create-react-app/docs/analyzing-the-bundle-size)
|
||||
|
||||
### Making a Progressive Web App
|
||||
|
||||
This section has moved here: [https://facebook.github.io/create-react-app/docs/making-a-progressive-web-app](https://facebook.github.io/create-react-app/docs/making-a-progressive-web-app)
|
||||
|
||||
### Advanced Configuration
|
||||
|
||||
This section has moved here: [https://facebook.github.io/create-react-app/docs/advanced-configuration](https://facebook.github.io/create-react-app/docs/advanced-configuration)
|
||||
|
||||
### Deployment
|
||||
|
||||
This section has moved here: [https://facebook.github.io/create-react-app/docs/deployment](https://facebook.github.io/create-react-app/docs/deployment)
|
||||
|
||||
### `npm run build` fails to minify
|
||||
|
||||
This section has moved here: [https://facebook.github.io/create-react-app/docs/troubleshooting#npm-run-build-fails-to-minify](https://facebook.github.io/create-react-app/docs/troubleshooting#npm-run-build-fails-to-minify)
|
17534
frontend-new/package-lock.json
generated
Normal file
17534
frontend-new/package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load Diff
57
frontend-new/package.json
Normal file
57
frontend-new/package.json
Normal file
@@ -0,0 +1,57 @@
|
||||
{
|
||||
"name": "frontend-new",
|
||||
"version": "0.1.0",
|
||||
"private": true,
|
||||
"dependencies": {
|
||||
"@ant-design/icons": "^6.0.0",
|
||||
"@testing-library/dom": "^10.4.0",
|
||||
"@testing-library/jest-dom": "^6.6.3",
|
||||
"@testing-library/react": "^16.3.0",
|
||||
"@testing-library/user-event": "^13.5.0",
|
||||
"antd": "^5.25.1",
|
||||
"axios": "^1.9.0",
|
||||
"dayjs": "^1.11.13",
|
||||
"echarts": "^5.6.0",
|
||||
"framer-motion": "^12.16.0",
|
||||
"http-proxy-middleware": "^3.0.5",
|
||||
"i18next": "^25.1.3",
|
||||
"moment": "^2.30.1",
|
||||
"react": "^19.1.0",
|
||||
"react-dom": "^19.1.0",
|
||||
"react-i18next": "^15.5.1",
|
||||
"react-redux": "^9.2.0",
|
||||
"react-router-dom": "^7.6.0",
|
||||
"react-scripts": "5.0.1",
|
||||
"react-toastify": "^11.0.5",
|
||||
"redux": "^5.0.1",
|
||||
"typescript": "^4.8.3",
|
||||
"web-vitals": "^2.1.4"
|
||||
},
|
||||
"scripts": {
|
||||
"start": "cross-env PORT=3003 react-scripts start",
|
||||
"build": "react-scripts build",
|
||||
"test": "react-scripts test",
|
||||
"eject": "react-scripts eject"
|
||||
},
|
||||
"eslintConfig": {
|
||||
"extends": [
|
||||
"react-app",
|
||||
"react-app/jest"
|
||||
]
|
||||
},
|
||||
"browserslist": {
|
||||
"production": [
|
||||
">0.2%",
|
||||
"not dead",
|
||||
"not op_mini all"
|
||||
],
|
||||
"development": [
|
||||
"last 1 chrome version",
|
||||
"last 1 firefox version",
|
||||
"last 1 safari version"
|
||||
]
|
||||
},
|
||||
"devDependencies": {
|
||||
"cross-env": "^7.0.3"
|
||||
}
|
||||
}
|
BIN
frontend-new/public/favicon.ico
Normal file
BIN
frontend-new/public/favicon.ico
Normal file
Binary file not shown.
After Width: | Height: | Size: 2.8 KiB |
@@ -13,4 +13,23 @@
|
||||
~ 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.
|
||||
-->
|
||||
-->
|
||||
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<link rel="shortcut icon" href="./favicon.ico" type="image/x-icon" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<meta name="description" content="" />
|
||||
<meta name="keywords" content="" />
|
||||
<meta http-equiv="X-UA-Compatible" content="IE=edge" />
|
||||
<title>RocketMQ Dashboard</title>
|
||||
</head>
|
||||
<body>
|
||||
<noscript>You need to enable JavaScript to run this app.</noscript>
|
||||
|
||||
<!-- React App will mount here -->
|
||||
<div id="root"></div>
|
||||
</body>
|
||||
</html>
|
3
frontend-new/public/robots.txt
Normal file
3
frontend-new/public/robots.txt
Normal file
@@ -0,0 +1,3 @@
|
||||
# https://www.robotstxt.org/robotstxt.html
|
||||
User-agent: *
|
||||
Disallow:
|
17
frontend-new/src/App.css
Normal file
17
frontend-new/src/App.css
Normal file
@@ -0,0 +1,17 @@
|
||||
/*
|
||||
* 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.
|
||||
*/
|
||||
|
@@ -14,41 +14,25 @@
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
.App {
|
||||
text-align: center;
|
||||
|
||||
import React from 'react';
|
||||
import AppRouter from './router'; // 你 router/index.jsx 导出的组件
|
||||
import { ToastContainer } from 'react-toastify';
|
||||
import 'react-toastify/dist/ReactToastify.css';
|
||||
import {ConfigProvider} from "antd";
|
||||
import {useTheme} from "./store/context/ThemeContext";
|
||||
|
||||
function App() {
|
||||
const {currentTheme} = useTheme();
|
||||
|
||||
return (
|
||||
<>
|
||||
<ConfigProvider theme={currentTheme}>
|
||||
<ToastContainer />
|
||||
<AppRouter />
|
||||
</ConfigProvider>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
.App-logo {
|
||||
height: 40vmin;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
@media (prefers-reduced-motion: no-preference) {
|
||||
.App-logo {
|
||||
animation: App-logo-spin infinite 20s linear;
|
||||
}
|
||||
}
|
||||
|
||||
.App-header {
|
||||
background-color: #282c34;
|
||||
min-height: 40vh;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font-size: calc(10px + 2vmin);
|
||||
color: white;
|
||||
}
|
||||
|
||||
.App-link {
|
||||
color: #61dafb;
|
||||
}
|
||||
|
||||
@keyframes App-logo-spin {
|
||||
from {
|
||||
transform: rotate(0deg);
|
||||
}
|
||||
to {
|
||||
transform: rotate(360deg);
|
||||
}
|
||||
}
|
||||
export default App;
|
@@ -14,6 +14,7 @@
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import { render, screen } from '@testing-library/react';
|
||||
import App from './App';
|
||||
|
942
frontend-new/src/api/remoteApi/remoteApi.js
Normal file
942
frontend-new/src/api/remoteApi/remoteApi.js
Normal file
@@ -0,0 +1,942 @@
|
||||
/*
|
||||
* 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.
|
||||
*/
|
||||
const appConfig = {
|
||||
apiBaseUrl: 'http://localhost:8082' // 请替换为你的实际 API Base URL
|
||||
};
|
||||
|
||||
let _redirectHandler = null;
|
||||
|
||||
const remoteApi = {
|
||||
|
||||
setRedirectHandler: (handler) => {
|
||||
_redirectHandler = handler;
|
||||
},
|
||||
|
||||
buildUrl: (endpoint) => {
|
||||
if (endpoint.charAt(0) === '/') {
|
||||
endpoint = endpoint.substring(1);
|
||||
}
|
||||
return `${appConfig.apiBaseUrl}/${endpoint}`;
|
||||
},
|
||||
|
||||
_fetch: async (url, options) => {
|
||||
try {
|
||||
// 在 options 中添加 credentials: 'include'
|
||||
const response = await fetch(url, {
|
||||
...options, // 保留原有的 options
|
||||
credentials: 'include' // 关键改动:允许发送 Cookie
|
||||
});
|
||||
|
||||
|
||||
// 检查响应是否被重定向,并且最终的 URL 包含了登录页的路径。
|
||||
// 这是会话过期或需要认证时后端重定向到登录页的常见模式。
|
||||
// 注意:fetch 会自动跟随 GET 请求的 3xx 重定向,所以我们检查的是 response.redirected。
|
||||
if (response.redirected) {
|
||||
if (_redirectHandler) {
|
||||
_redirectHandler(); // 如果设置了重定向处理函数,则调用它
|
||||
}
|
||||
return { __isRedirectHandled: true };
|
||||
}
|
||||
|
||||
return response;
|
||||
} catch (error) {
|
||||
console.error("Fetch 请求出错:", error);
|
||||
throw error;
|
||||
}
|
||||
},
|
||||
|
||||
queryTopic: async (skipSysProcess) => {
|
||||
try {
|
||||
const params = new URLSearchParams();
|
||||
if (skipSysProcess) {
|
||||
params.append('skipSysProcess', 'true');
|
||||
}
|
||||
|
||||
const response = await remoteApi._fetch(remoteApi.buildUrl(`/topic/list.query?${params.toString()}`));
|
||||
const data = await response.json();
|
||||
return data;
|
||||
} catch (error) {
|
||||
console.error("Error fetching topic list:", error);
|
||||
}
|
||||
},
|
||||
|
||||
listUsers: async (brokerAddress) => {
|
||||
const params = new URLSearchParams();
|
||||
if (brokerAddress) params.append('brokerAddress', brokerAddress);
|
||||
const response = await remoteApi._fetch(remoteApi.buildUrl(`/acl/listUsers?${params.toString()}`));
|
||||
return await response.json();
|
||||
},
|
||||
|
||||
createUser: async (brokerAddress, userInfo) => {
|
||||
const response = await remoteApi._fetch(remoteApi.buildUrl('/acl/createUser'), {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ brokerAddress, userInfo })
|
||||
});
|
||||
return await response.json(); // 返回字符串消息
|
||||
},
|
||||
|
||||
updateUser: async (brokerAddress, userInfo) => {
|
||||
const response = await remoteApi._fetch(remoteApi.buildUrl('/acl/updateUser'), {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ brokerAddress, userInfo })
|
||||
});
|
||||
return await response.json();
|
||||
},
|
||||
|
||||
deleteUser: async (brokerAddress, username) => {
|
||||
const params = new URLSearchParams();
|
||||
if (brokerAddress) params.append('brokerAddress', brokerAddress);
|
||||
params.append('username', username);
|
||||
const response = await remoteApi._fetch(remoteApi.buildUrl(`/acl/deleteUser?${params.toString()}`), {
|
||||
method: 'DELETE'
|
||||
});
|
||||
return await response.json();
|
||||
},
|
||||
|
||||
// --- ACL 权限相关 API ---
|
||||
listAcls: async (brokerAddress, searchParam) => {
|
||||
const params = new URLSearchParams();
|
||||
if (brokerAddress) params.append('brokerAddress', brokerAddress);
|
||||
if (searchParam) params.append('searchParam', searchParam);
|
||||
const response = await remoteApi._fetch(remoteApi.buildUrl(`/acl/listAcls?${params.toString()}`));
|
||||
return await response.json();
|
||||
},
|
||||
|
||||
createAcl: async (brokerAddress, subject, policies) => {
|
||||
const response = await remoteApi._fetch(remoteApi.buildUrl('/acl/createAcl'), {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ brokerAddress, subject, policies })
|
||||
});
|
||||
return await response.json();
|
||||
},
|
||||
|
||||
updateAcl: async (brokerAddress, subject, policies) => {
|
||||
const response = await remoteApi._fetch(remoteApi.buildUrl('/acl/updateAcl'), {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ brokerAddress, subject, policies })
|
||||
});
|
||||
return await response.json();
|
||||
},
|
||||
|
||||
deleteAcl: async (brokerAddress, subject, resource) => {
|
||||
const params = new URLSearchParams();
|
||||
if (brokerAddress) params.append('brokerAddress', brokerAddress);
|
||||
params.append('subject', subject);
|
||||
if (resource) params.append('resource', resource);
|
||||
const response = await remoteApi._fetch(remoteApi.buildUrl(`/acl/deleteAcl?${params.toString()}`), {
|
||||
method: 'DELETE'
|
||||
});
|
||||
return await response.json();
|
||||
},
|
||||
|
||||
|
||||
queryMessageByMessageId: async (msgId, topic, callback) => {
|
||||
try {
|
||||
const params = new URLSearchParams();
|
||||
params.append('msgId', msgId);
|
||||
params.append('topic', topic);
|
||||
|
||||
const response = await remoteApi._fetch(remoteApi.buildUrl(`/messageTrace/viewMessage.query?${params.toString()}`));
|
||||
const data = await response.json();
|
||||
return data
|
||||
} catch (error) {
|
||||
console.error("Error querying message by ID:", error);
|
||||
callback({ status: 1, errMsg: "Failed to query message by ID" });
|
||||
}
|
||||
},
|
||||
|
||||
queryMessageTraceByMessageId: async (msgId, traceTopic, callback) => {
|
||||
try {
|
||||
const params = new URLSearchParams();
|
||||
params.append('msgId', msgId);
|
||||
params.append('traceTopic', traceTopic);
|
||||
|
||||
const response = await remoteApi._fetch(remoteApi.buildUrl(`/messageTrace/viewMessageTraceGraph.query?${params.toString()}`));
|
||||
const data = await response.json();
|
||||
return data;
|
||||
} catch (error) {
|
||||
console.error("Error querying message trace:", error);
|
||||
|
||||
}
|
||||
},
|
||||
queryDlqMessageByConsumerGroup: async (consumerGroup, beginTime, endTime, pageNum, pageSize, taskId) => {
|
||||
try {
|
||||
const response = await remoteApi._fetch(remoteApi.buildUrl("/dlqMessage/queryDlqMessageByConsumerGroup.query"), {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
body: JSON.stringify({
|
||||
topic: `%DLQ%${consumerGroup}`,
|
||||
begin: beginTime,
|
||||
end: endTime,
|
||||
pageNum: pageNum,
|
||||
pageSize: pageSize,
|
||||
taskId: taskId,
|
||||
}),
|
||||
});
|
||||
const data = await response.json();
|
||||
return data;
|
||||
} catch (error) {
|
||||
console.error("Error querying DLQ messages by consumer group:", error);
|
||||
return { status: 1, errMsg: "Failed to query DLQ messages by consumer group" };
|
||||
}
|
||||
},
|
||||
resendDlqMessage: async (msgId, consumerGroup, topic) => {
|
||||
try {
|
||||
const response = await remoteApi._fetch(remoteApi.buildUrl("/message/consumeMessageDirectly.do"), {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
params: {
|
||||
msgId: msgId,
|
||||
consumerGroup: consumerGroup,
|
||||
topic: topic
|
||||
},
|
||||
});
|
||||
const data = await response.json();
|
||||
return data;
|
||||
} catch (error) {
|
||||
console.error("Error resending DLQ message:", error);
|
||||
return { status: 1, errMsg: "Failed to resend DLQ message" };
|
||||
}
|
||||
},
|
||||
exportDlqMessage: async (msgId, consumerGroup) => {
|
||||
try {
|
||||
const response = await remoteApi._fetch(remoteApi.buildUrl(`/dlqMessage/exportDlqMessage.do?msgId=${msgId}&consumerGroup=${consumerGroup}`));
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(`HTTP error! status: ${response.status}`);
|
||||
}
|
||||
|
||||
// 假设服务器总是返回 JSON
|
||||
const data = await response.json();
|
||||
|
||||
// 1. 打开一个新的空白窗口
|
||||
const newWindow = window.open('', '_blank');
|
||||
|
||||
if (!newWindow) {
|
||||
// 浏览器可能会阻止弹窗,需要用户允许
|
||||
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('<h1>DLQ 导出 JSON 内容</h1>');
|
||||
// 使用 <pre> 标签保持格式,并使用 JSON.stringify 格式化 JSON 以便于阅读
|
||||
newWindow.document.write('<pre>' + JSON.stringify(data, null, 2) + '</pre>');
|
||||
newWindow.document.write('</body></html>');
|
||||
newWindow.document.close(); // 关闭文档流,确保内容显示
|
||||
|
||||
return { status: 0, msg: "导出请求成功,内容已在新页面显示" };
|
||||
} catch (error) {
|
||||
console.error("Error exporting DLQ message:", error);
|
||||
return { status: 1, errMsg: "Failed to export DLQ message: " + error.message };
|
||||
}
|
||||
},
|
||||
|
||||
batchResendDlqMessage: async (messages) => {
|
||||
try {
|
||||
const response = await remoteApi._fetch(remoteApi.buildUrl("/dlqMessage/batchResendDlqMessage.do"), {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
body: JSON.stringify(messages),
|
||||
});
|
||||
const data = await response.json();
|
||||
return data;
|
||||
} catch (error) {
|
||||
console.error("Error batch resending DLQ messages:", error);
|
||||
return { status: 1, errMsg: "Failed to batch resend DLQ messages" };
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Queries messages by topic with pagination.
|
||||
* @param {string} topic The topic to query.
|
||||
* @param {number} begin Timestamp in milliseconds for the start time.
|
||||
* @param {number} end Timestamp in milliseconds for the end time.
|
||||
* @param {number} pageNum The current page number (1-based).
|
||||
* @param {number} pageSize The number of items per page.
|
||||
* @param {string} taskId Optional task ID for continuous queries.
|
||||
* @returns {Promise<Object>} The API response.
|
||||
*/
|
||||
queryMessagePageByTopic: async (topic, begin, end, pageNum, pageSize, taskId) => {
|
||||
try {
|
||||
const response = await remoteApi._fetch(remoteApi.buildUrl("/message/queryMessagePageByTopic.query"), {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
body: JSON.stringify({
|
||||
topic: topic,
|
||||
begin: begin,
|
||||
end: end,
|
||||
pageNum: pageNum,
|
||||
pageSize: pageSize,
|
||||
taskId: taskId
|
||||
})
|
||||
});
|
||||
const data = await response.json();
|
||||
return data;
|
||||
} catch (error) {
|
||||
console.error("Error fetching message page by topic:", error);
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Queries messages by topic and key.
|
||||
* @param {string} topic The topic to query.
|
||||
* @param {string} key The message key to query.
|
||||
* @returns {Promise<Object>} The API response.
|
||||
*/
|
||||
queryMessageByTopicAndKey: async (topic, key) => {
|
||||
try {
|
||||
const response = await remoteApi._fetch(remoteApi.buildUrl(`/message/queryMessageByTopicAndKey.query?topic=${topic}&key=${key}`));
|
||||
const data = await response.json();
|
||||
return data;
|
||||
} catch (error) {
|
||||
console.error("Error fetching message by topic and key:", error);
|
||||
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Views a message by its message ID and topic.
|
||||
* @param {string} msgId The message ID.
|
||||
* @param {string} topic The topic of the message.
|
||||
* @returns {Promise<Object>} The API response.
|
||||
*/
|
||||
viewMessage: async (msgId, topic) => {
|
||||
try {
|
||||
const encodedTopic = encodeURIComponent(topic);
|
||||
|
||||
const url = remoteApi.buildUrl(
|
||||
`/message/viewMessage.query?msgId=${msgId}&topic=${encodedTopic}`
|
||||
);
|
||||
const response = await remoteApi._fetch(url);
|
||||
const data = await response.json();
|
||||
return data;
|
||||
} catch (error) {
|
||||
console.error("Error fetching message by message ID:", error);
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Resends a message directly to a consumer group.
|
||||
* @param {string} msgId The message ID.
|
||||
* @param {string} consumerGroup The consumer group to resend to.
|
||||
* @param {string} topic The topic of the message.
|
||||
* @returns {Promise<Object>} The API response.
|
||||
*/
|
||||
resendMessageDirectly: async (msgId, consumerGroup, topic) => {
|
||||
topic = encodeURIComponent(topic)
|
||||
try {
|
||||
const response = await remoteApi._fetch(remoteApi.buildUrl(`/message/consumeMessageDirectly.do?msgId=${msgId}&consumerGroup=${consumerGroup}&topic=${topic}`), {
|
||||
method: 'POST',
|
||||
});
|
||||
const data = await response.json();
|
||||
return data;
|
||||
} catch (error) {
|
||||
console.error("Error resending message directly:", error);
|
||||
|
||||
}
|
||||
},
|
||||
|
||||
queryProducerConnection: async (topic, producerGroup, callback) => {
|
||||
topic = encodeURIComponent(topic)
|
||||
producerGroup = encodeURIComponent(producerGroup)
|
||||
try {
|
||||
const response = await remoteApi._fetch(remoteApi.buildUrl(`/producer/producerConnection.query?topic=${topic}&producerGroup=${producerGroup}`));
|
||||
const data = await response.json();
|
||||
callback(data);
|
||||
} catch (error) {
|
||||
console.error("Error fetching producer connection list:", error);
|
||||
callback({ status: 1, errMsg: "Failed to fetch producer connection list" }); // Simulate error response
|
||||
}
|
||||
},
|
||||
|
||||
queryConsumerGroupList: async (skipSysGroup = false) => {
|
||||
try {
|
||||
const response = await remoteApi._fetch(remoteApi.buildUrl(`/consumer/groupList.query?skipSysGroup=${skipSysGroup}`));
|
||||
const data = await response.json();
|
||||
return data;
|
||||
} catch (error) {
|
||||
console.error("Error fetching consumer group list:", error);
|
||||
return { status: 1, errMsg: "Failed to fetch consumer group list" };
|
||||
}
|
||||
},
|
||||
|
||||
refreshConsumerGroup: async (consumerGroup) => {
|
||||
try {
|
||||
const response = await remoteApi._fetch(remoteApi.buildUrl(`/consumer/group.refresh?consumerGroup=${consumerGroup}`));
|
||||
const data = await response.json();
|
||||
return data;
|
||||
} catch (error) {
|
||||
console.error(`Error refreshing consumer group ${consumerGroup}:`, error);
|
||||
return { status: 1, errMsg: `Failed to refresh consumer group ${consumerGroup}` };
|
||||
}
|
||||
},
|
||||
|
||||
refreshAllConsumerGroup: async () => {
|
||||
try {
|
||||
const response = await remoteApi._fetch(remoteApi.buildUrl("/consumer/group.refresh.all"));
|
||||
const data = await response.json();
|
||||
return data;
|
||||
} catch (error) {
|
||||
console.error("Error refreshing all consumer groups:", error);
|
||||
return { status: 1, errMsg: "Failed to refresh all consumer groups" };
|
||||
}
|
||||
},
|
||||
|
||||
queryConsumerMonitorConfig: async (consumeGroupName) => {
|
||||
try {
|
||||
const response = await remoteApi._fetch(remoteApi.buildUrl(`/monitor/consumerMonitorConfigByGroupName.query?consumeGroupName=${consumeGroupName}`));
|
||||
const data = await response.json();
|
||||
return data;
|
||||
} catch (error) {
|
||||
console.error(`Error fetching monitor config for ${consumeGroupName}:`, error);
|
||||
return { status: 1, errMsg: `Failed to fetch monitor config for ${consumeGroupName}` };
|
||||
}
|
||||
},
|
||||
|
||||
createOrUpdateConsumerMonitor: async (consumeGroupName, minCount, maxDiffTotal) => {
|
||||
consumeGroupName = encodeURIComponent(consumeGroupName)
|
||||
try {
|
||||
const response = await remoteApi._fetch(remoteApi.buildUrl("/monitor/createOrUpdateConsumerMonitor.do"), {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json'
|
||||
},
|
||||
body: JSON.stringify({ consumeGroupName, minCount, maxDiffTotal })
|
||||
});
|
||||
const data = await response.json();
|
||||
return data;
|
||||
} catch (error) {
|
||||
console.error("Error creating or updating consumer monitor:", error);
|
||||
return { status: 1, errMsg: "Failed to create or update consumer monitor" };
|
||||
}
|
||||
},
|
||||
|
||||
fetchBrokerNameList: async (consumerGroup) => {
|
||||
try {
|
||||
const response = await remoteApi._fetch(remoteApi.buildUrl(`/consumer/fetchBrokerNameList.query?consumerGroup=${consumerGroup}`));
|
||||
const data = await response.json();
|
||||
return data;
|
||||
} catch (error) {
|
||||
console.error(`Error fetching broker name list for ${consumerGroup}:`, error);
|
||||
return { status: 1, errMsg: `Failed to fetch broker name list for ${consumerGroup}` };
|
||||
}
|
||||
},
|
||||
|
||||
deleteConsumerGroup: async (groupName, brokerNameList) => {
|
||||
try {
|
||||
const response = await remoteApi._fetch(remoteApi.buildUrl("/consumer/deleteSubGroup.do"), {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json'
|
||||
},
|
||||
body: JSON.stringify({ groupName, brokerNameList })
|
||||
});
|
||||
const data = await response.json();
|
||||
return data;
|
||||
} catch (error) {
|
||||
console.error(`Error deleting consumer group ${groupName}:`, error);
|
||||
return { status: 1, errMsg: `Failed to delete consumer group ${groupName}` };
|
||||
}
|
||||
},
|
||||
|
||||
queryConsumerConfig: async (consumerGroup) => {
|
||||
try {
|
||||
const response = await remoteApi._fetch(remoteApi.buildUrl(`/consumer/examineSubscriptionGroupConfig.query?consumerGroup=${consumerGroup}`));
|
||||
const data = await response.json();
|
||||
return data;
|
||||
} catch (error) {
|
||||
console.error(`Error fetching consumer config for ${consumerGroup}:`, error);
|
||||
return { status: 1, errMsg: `Failed to fetch consumer config for ${consumerGroup}` };
|
||||
}
|
||||
},
|
||||
|
||||
createOrUpdateConsumer: async (consumerRequest) => {
|
||||
try {
|
||||
const response = await remoteApi._fetch(remoteApi.buildUrl("/consumer/createOrUpdate.do"), {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json'
|
||||
},
|
||||
body: JSON.stringify(consumerRequest)
|
||||
});
|
||||
const data = await response.json();
|
||||
return data;
|
||||
} catch (error) {
|
||||
console.error("Error creating or updating consumer:", error);
|
||||
return { status: 1, errMsg: "Failed to create or update consumer" };
|
||||
}
|
||||
},
|
||||
|
||||
queryTopicByConsumer: async (consumerGroup, address) => {
|
||||
try {
|
||||
const response = await remoteApi._fetch(remoteApi.buildUrl(`/consumer/queryTopicByConsumer.query?consumerGroup=${consumerGroup}&address=${address}`));
|
||||
const data = await response.json();
|
||||
return data;
|
||||
} catch (error) {
|
||||
console.error(`Error fetching topics for consumer group ${consumerGroup}:`, error);
|
||||
return { status: 1, errMsg: `Failed to fetch topics for consumer group ${consumerGroup}` };
|
||||
}
|
||||
},
|
||||
|
||||
queryConsumerConnection: async (consumerGroup, address) => {
|
||||
try {
|
||||
const response = await remoteApi._fetch(remoteApi.buildUrl(`/consumer/consumerConnection.query?consumerGroup=${consumerGroup}&address=${address}`));
|
||||
const data = await response.json();
|
||||
return data;
|
||||
} catch (error) {
|
||||
console.error(`Error fetching consumer connections for ${consumerGroup}:`, error);
|
||||
return { status: 1, errMsg: `Failed to fetch consumer connections for ${consumerGroup}` };
|
||||
}
|
||||
},
|
||||
|
||||
queryConsumerRunningInfo: async (consumerGroup, clientId, jstack = false) => {
|
||||
try {
|
||||
const response = await remoteApi._fetch(remoteApi.buildUrl(`/consumer/consumerRunningInfo.query?consumerGroup=${consumerGroup}&clientId=${clientId}&jstack=${jstack}`));
|
||||
const data = await response.json();
|
||||
return data;
|
||||
} catch (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}` };
|
||||
}
|
||||
},
|
||||
queryTopicList: async () => {
|
||||
try {
|
||||
const response = await remoteApi._fetch(remoteApi.buildUrl("/topic/list.queryTopicType"));
|
||||
return await response.json();
|
||||
} catch (error) {
|
||||
console.error("Error fetching topic list:", error);
|
||||
return { status: 1, errMsg: "Failed to fetch topic list" };
|
||||
}
|
||||
},
|
||||
|
||||
deleteTopic: async (topic) => {
|
||||
try {
|
||||
const url = remoteApi.buildUrl(`/topic/deleteTopic.do?topic=${encodeURIComponent(topic)}`);
|
||||
const response = await remoteApi._fetch(url, {
|
||||
method: 'POST', // 仍然使用 POST 方法,但参数在 URL 中
|
||||
headers: {
|
||||
'Content-Type': 'application/json', // 可以根据你的后端需求决定是否需要这个 header
|
||||
},
|
||||
// body: JSON.stringify({ topic }) // 移除 body
|
||||
});
|
||||
return await response.json();
|
||||
} catch (error) {
|
||||
console.error("Error deleting topic:", error);
|
||||
return { status: 1, errMsg: "Failed to delete topic" };
|
||||
}
|
||||
},
|
||||
|
||||
getTopicStats: async (topic) => {
|
||||
try {
|
||||
const response = await remoteApi._fetch(remoteApi.buildUrl(`/topic/stats.query?topic=${topic}`));
|
||||
return await response.json();
|
||||
} catch (error) {
|
||||
console.error("Error fetching topic stats:", error);
|
||||
return { status: 1, errMsg: "Failed to fetch topic stats" };
|
||||
}
|
||||
},
|
||||
|
||||
getTopicRoute: async (topic) => {
|
||||
try {
|
||||
const response = await remoteApi._fetch(remoteApi.buildUrl(`/topic/route.query?topic=${topic}`));
|
||||
return await response.json();
|
||||
} catch (error) {
|
||||
console.error("Error fetching topic route:", error);
|
||||
return { status: 1, errMsg: "Failed to fetch topic route" };
|
||||
}
|
||||
},
|
||||
|
||||
getTopicConsumers: async (topic) => {
|
||||
try {
|
||||
const response = await remoteApi._fetch(remoteApi.buildUrl(`/topic/queryConsumerByTopic.query?topic=${topic}`));
|
||||
return await response.json();
|
||||
} catch (error) {
|
||||
console.error("Error fetching topic consumers:", error);
|
||||
return { status: 1, errMsg: "Failed to fetch topic consumers" };
|
||||
}
|
||||
},
|
||||
|
||||
getTopicConsumerGroups: async (topic) => {
|
||||
try {
|
||||
const response = await remoteApi._fetch(remoteApi.buildUrl(`/topic/queryTopicConsumerInfo.query?topic=${topic}`));
|
||||
return await response.json();
|
||||
} catch (error) {
|
||||
console.error("Error fetching consumer groups:", error);
|
||||
return { status: 1, errMsg: "Failed to fetch consumer groups" };
|
||||
}
|
||||
},
|
||||
|
||||
getTopicConfig: async (topic) => {
|
||||
try {
|
||||
const response = await remoteApi._fetch(remoteApi.buildUrl(`/topic/examineTopicConfig.query?topic=${topic}`));
|
||||
return await response.json();
|
||||
} catch (error) {
|
||||
console.error("Error fetching topic config:", error);
|
||||
return { status: 1, errMsg: "Failed to fetch topic config" };
|
||||
}
|
||||
},
|
||||
|
||||
getClusterList: async () => {
|
||||
try {
|
||||
const response = await remoteApi._fetch(remoteApi.buildUrl("/cluster/list.query"));
|
||||
return await response.json();
|
||||
} catch (error) {
|
||||
console.error("Error fetching cluster list:", error);
|
||||
return { status: 1, errMsg: "Failed to fetch cluster list" };
|
||||
}
|
||||
},
|
||||
|
||||
createOrUpdateTopic: async (topicData) => {
|
||||
try {
|
||||
const response = await remoteApi._fetch(remoteApi.buildUrl("/topic/createOrUpdate.do"), {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
body: JSON.stringify(topicData)
|
||||
});
|
||||
return await response.json();
|
||||
} catch (error) {
|
||||
console.error("Error creating/updating topic:", error);
|
||||
return { status: 1, errMsg: "Failed to create/update topic" };
|
||||
}
|
||||
},
|
||||
|
||||
resetConsumerOffset: async (data) => {
|
||||
try {
|
||||
const response = await remoteApi._fetch(remoteApi.buildUrl("/consumer/resetOffset.do"), {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
body: JSON.stringify(data)
|
||||
});
|
||||
return await response.json();
|
||||
} catch (error) {
|
||||
console.error("Error resetting consumer offset:", error);
|
||||
return { status: 1, errMsg: "Failed to reset consumer offset" };
|
||||
}
|
||||
},
|
||||
|
||||
skipMessageAccumulate: async (data) => {
|
||||
try {
|
||||
const response = await remoteApi._fetch(remoteApi.buildUrl("/consumer/skipAccumulate.do"), {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
body: JSON.stringify(data)
|
||||
});
|
||||
return await response.json();
|
||||
} catch (error) {
|
||||
console.error("Error skipping message accumulate:", error);
|
||||
return { status: 1, errMsg: "Failed to skip message accumulate" };
|
||||
}
|
||||
},
|
||||
|
||||
sendTopicMessage: async (messageData) => {
|
||||
try {
|
||||
const response = await remoteApi._fetch(remoteApi.buildUrl("/topic/sendTopicMessage.do"), {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
body: JSON.stringify(messageData)
|
||||
});
|
||||
return await response.json();
|
||||
} catch (error) {
|
||||
console.error("Error sending topic message:", error);
|
||||
return { status: 1, errMsg: "Failed to send topic message" };
|
||||
}
|
||||
},
|
||||
|
||||
deleteTopicByBroker: async (brokerName, topic) => {
|
||||
try {
|
||||
const response = await remoteApi._fetch(remoteApi.buildUrl("/topic/deleteTopicByBroker.do"), {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
body: JSON.stringify({ brokerName, topic })
|
||||
});
|
||||
return await response.json();
|
||||
} catch (error) {
|
||||
console.error("Error deleting topic by broker:", error);
|
||||
return { status: 1, errMsg: "Failed to delete topic by broker" };
|
||||
}
|
||||
},
|
||||
|
||||
// New API methods for Ops page
|
||||
queryOpsHomePage: async () => {
|
||||
try {
|
||||
const response = await remoteApi._fetch(remoteApi.buildUrl("/ops/homePage.query"));
|
||||
return await response.json();
|
||||
} catch (error) {
|
||||
console.error("Error fetching ops home page data:", error);
|
||||
return { status: 1, errMsg: "Failed to fetch ops home page data" };
|
||||
}
|
||||
},
|
||||
|
||||
updateNameSvrAddr: async (nameSvrAddr) => {
|
||||
try {
|
||||
const response = await remoteApi._fetch(remoteApi.buildUrl(`/ops/updateNameSvrAddr.do?nameSvrAddrList=${encodeURIComponent(nameSvrAddr)}`), {
|
||||
method: 'POST',
|
||||
});
|
||||
return await response.json();
|
||||
} catch (error) {
|
||||
console.error("Error updating NameServer address:", error);
|
||||
return { status: 1, errMsg: "Failed to update NameServer address" };
|
||||
}
|
||||
},
|
||||
|
||||
addNameSvrAddr: async (newNamesrvAddr) => {
|
||||
try {
|
||||
const response = await remoteApi._fetch(remoteApi.buildUrl(`/ops/addNameSvrAddr.do?newNamesrvAddr=${encodeURIComponent(newNamesrvAddr)}`), {
|
||||
method: 'POST',
|
||||
});
|
||||
return await response.json();
|
||||
} catch (error) {
|
||||
console.error("Error adding NameServer address:", error);
|
||||
return { status: 1, errMsg: "Failed to add NameServer address" };
|
||||
}
|
||||
},
|
||||
|
||||
updateIsVIPChannel: async (useVIPChannel) => {
|
||||
try {
|
||||
const response = await remoteApi._fetch(remoteApi.buildUrl(`/ops/updateIsVIPChannel.do?useVIPChannel=${useVIPChannel}`), {
|
||||
method: 'POST',
|
||||
});
|
||||
return await response.json();
|
||||
} catch (error) {
|
||||
console.error("Error updating VIP Channel status:", error);
|
||||
return { status: 1, errMsg: "Failed to update VIP Channel status" };
|
||||
}
|
||||
},
|
||||
|
||||
updateUseTLS: async (useTLS) => {
|
||||
try {
|
||||
const response = await remoteApi._fetch(remoteApi.buildUrl(`/ops/updateUseTLS.do?useTLS=${useTLS}`), {
|
||||
method: 'POST',
|
||||
});
|
||||
return await response.json();
|
||||
} catch (error) {
|
||||
console.error("Error updating TLS status:", error);
|
||||
return { status: 1, errMsg: "Failed to update TLS status" };
|
||||
}
|
||||
},
|
||||
|
||||
queryClusterList: async (callback) => {
|
||||
try {
|
||||
const response = await remoteApi._fetch(remoteApi.buildUrl("/cluster/list.query"));
|
||||
const data = await response.json();
|
||||
callback(data);
|
||||
} catch (error) {
|
||||
console.error("Error fetching cluster list:", error);
|
||||
callback({ status: 1, errMsg: "Failed to fetch cluster list" });
|
||||
}
|
||||
},
|
||||
|
||||
queryBrokerHisData: async (date, callback) => {
|
||||
try {
|
||||
const url = new URL(remoteApi.buildUrl('/dashboard/broker.query'));
|
||||
url.searchParams.append('date', date);
|
||||
const response = await remoteApi._fetch(url.toString(), { signal: AbortSignal.timeout(15000) }); // 15s timeout
|
||||
const data = await response.json();
|
||||
callback(data);
|
||||
} catch (error) {
|
||||
if (error.name === 'TimeoutError') {
|
||||
console.error("Broker history data request timed out:", error);
|
||||
callback({ status: 1, errMsg: "Request timed out for broker history data" });
|
||||
} else {
|
||||
console.error("Error fetching broker history data:", error);
|
||||
callback({ status: 1, errMsg: "Failed to fetch broker history data" });
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
queryTopicHisData: async (date, topicName, callback) => {
|
||||
try {
|
||||
const url = new URL(remoteApi.buildUrl('/dashboard/topic.query'));
|
||||
url.searchParams.append('date', date);
|
||||
url.searchParams.append('topicName', topicName);
|
||||
const response = await remoteApi._fetch(url.toString(), { signal: AbortSignal.timeout(15000) }); // 15s timeout
|
||||
const data = await response.json();
|
||||
callback(data);
|
||||
} catch (error) {
|
||||
if (error.name === 'TimeoutError') {
|
||||
console.error("Topic history data request timed out:", error);
|
||||
callback({ status: 1, errMsg: "Request timed out for topic history data" });
|
||||
} else {
|
||||
console.error("Error fetching topic history data:", error);
|
||||
callback({ status: 1, errMsg: "Failed to fetch topic history data" });
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
queryTopicCurrentData: async (callback) => {
|
||||
try {
|
||||
const response = await remoteApi._fetch(remoteApi.buildUrl('/dashboard/topicCurrent.query'), { signal: AbortSignal.timeout(15000) }); // 15s timeout
|
||||
const data = await response.json();
|
||||
callback(data);
|
||||
} catch (error) {
|
||||
if (error.name === 'TimeoutError') {
|
||||
console.error("Topic current data request timed out:", error);
|
||||
callback({ status: 1, errMsg: "Request timed out for topic current data" });
|
||||
} else {
|
||||
console.error("Error fetching topic current data:", error);
|
||||
callback({ status: 1, errMsg: "Failed to fetch topic current data" });
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
queryBrokerConfig: async (brokerAddr, callback) => {
|
||||
try {
|
||||
const url = new URL(remoteApi.buildUrl('/cluster/brokerConfig.query'));
|
||||
url.searchParams.append('brokerAddr', brokerAddr);
|
||||
const response = await remoteApi._fetch(url.toString());
|
||||
const data = await response.json();
|
||||
callback(data);
|
||||
} catch (error) {
|
||||
console.error("Error fetching broker config:", error);
|
||||
callback({ status: 1, errMsg: "Failed to fetch broker config" });
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* 查询 Proxy 首页信息,包括地址列表和当前 Proxy 地址
|
||||
*/
|
||||
queryProxyHomePage: async (callback) => {
|
||||
try {
|
||||
const response = await remoteApi._fetch(remoteApi.buildUrl("/proxy/homePage.query"));
|
||||
const data = await response.json();
|
||||
callback(data);
|
||||
} catch (error) {
|
||||
console.error("Error fetching proxy home page:", error);
|
||||
callback({ status: 1, errMsg: "Failed to fetch proxy home page" });
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* 添加新的 Proxy 地址
|
||||
*/
|
||||
addProxyAddr: async (newProxyAddr, callback) => {
|
||||
try {
|
||||
const response = await remoteApi._fetch(remoteApi.buildUrl("/proxy/addProxyAddr.do"), {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
|
||||
body: new URLSearchParams({ newProxyAddr }).toString()
|
||||
});
|
||||
const data = await response.json();
|
||||
callback(data);
|
||||
} catch (error) {
|
||||
console.error("Error adding proxy address:", error);
|
||||
callback({ status: 1, errMsg: "Failed to add proxy address" });
|
||||
}
|
||||
},
|
||||
login: async (username, password) => {
|
||||
try {
|
||||
|
||||
|
||||
// 2. 发送请求,注意 body 可以是空字符串或 null,或者直接省略 body
|
||||
// 这里使用 GET 方法,因为参数在 URL 上
|
||||
const response = await remoteApi._fetch(remoteApi.buildUrl("/login/login.do"), {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/x-www-form-urlencoded' // 这个 header 可能不再需要,或者需要调整
|
||||
},
|
||||
body: new URLSearchParams({
|
||||
username: username, // 假设 username 是变量名
|
||||
password: password // 假设 password 是变量名
|
||||
}).toString()
|
||||
});
|
||||
|
||||
// 3. 处理响应
|
||||
const data = await response.json();
|
||||
return data;
|
||||
} catch (error) {
|
||||
console.error("Error logging in:", error);
|
||||
return { status: 1, errMsg: "Failed to log in" };
|
||||
}
|
||||
},
|
||||
|
||||
logout: async () => {
|
||||
try {
|
||||
const response = await remoteApi._fetch(remoteApi.buildUrl("/login/logout.do"),{
|
||||
method: 'POST'
|
||||
});
|
||||
return await response.json()
|
||||
}catch (error) {
|
||||
console.error("Error logging out:", error);
|
||||
return { status: 1, errMsg: "Failed to log out" };
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const tools = {
|
||||
// 适配新的数据结构
|
||||
dashboardRefreshTime: 5000,
|
||||
generateBrokerMap: (brokerServer, clusterAddrTable, brokerAddrTable) => {
|
||||
const clusterMap = {}; // 最终存储 { clusterName: [brokerInstance1, brokerInstance2, ...] }
|
||||
|
||||
Object.entries(clusterAddrTable).forEach(([clusterName, brokerNamesInCluster]) => {
|
||||
clusterMap[clusterName] = []; // 初始化当前集群的 broker 列表
|
||||
|
||||
brokerNamesInCluster.forEach(brokerName => {
|
||||
// 从 brokerAddrTable 获取当前 brokerName 下的所有 brokerId 及其地址
|
||||
const brokerAddrs = brokerAddrTable[brokerName]?.brokerAddrs; // 确保 brokerAddrs 存在
|
||||
if (brokerAddrs) {
|
||||
Object.entries(brokerAddrs).forEach(([brokerIdStr, address]) => {
|
||||
const brokerId = parseInt(brokerIdStr); // brokerId 是字符串,转为数字
|
||||
// 从 brokerServer 获取当前 brokerName 和 brokerId 对应的详细信息
|
||||
const detail = brokerServer[brokerName]?.[brokerIdStr];
|
||||
|
||||
if (detail) {
|
||||
clusterMap[clusterName].push({
|
||||
brokerName: brokerName,
|
||||
brokerId: brokerId,
|
||||
address: address,
|
||||
...detail,
|
||||
detail: detail,
|
||||
brokerConfig: {},
|
||||
});
|
||||
} else {
|
||||
console.warn(`No detail found for broker: ${brokerName} with ID: ${brokerIdStr}`);
|
||||
}
|
||||
});
|
||||
} else {
|
||||
console.warn(`No addresses found for brokerName: ${brokerName} in brokerAddrTable`);
|
||||
}
|
||||
});
|
||||
});
|
||||
return clusterMap;
|
||||
}
|
||||
};
|
||||
|
||||
export { remoteApi, tools };
|
108
frontend-new/src/assets/styles/theme.js
Normal file
108
frontend-new/src/assets/styles/theme.js
Normal file
@@ -0,0 +1,108 @@
|
||||
/*
|
||||
* 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.
|
||||
*/
|
||||
export const defaultTheme = {
|
||||
token: {
|
||||
colorPrimary: '#0cb5fb', // 主题色
|
||||
borderRadius: 1.5, // 组件圆角
|
||||
},
|
||||
components: {
|
||||
Button: {
|
||||
colorPrimary: '#1c324a', // 普通按钮主题色
|
||||
},
|
||||
Layout: {
|
||||
headerBg: '#1c324a', // 设置 Header 的背景色为 #1c324a
|
||||
headerColor: '#ffffff', // 设置 Header 内文本颜色为白色
|
||||
backgroundColor: '#ffffff', // 设置 Layout 的背景色为白色
|
||||
colorBgLayout: '#f9fcfe',
|
||||
},
|
||||
Menu: {
|
||||
darkItemBg: '#1c324a',
|
||||
horizontalItemSelectedBg: '#0cb5fb',
|
||||
itemSelectedColor: '#ffffff',
|
||||
itemColor: '#ffffff',
|
||||
colorText: 'rgba(255, 255, 255, 0.88)', // Adjust for dark theme menu
|
||||
activeBarBorderWidth: 0,
|
||||
},
|
||||
Drawer: {
|
||||
colorBgElevated: '#1c324a', // Drawer 背景色
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
export const pinkTheme = {
|
||||
token: {
|
||||
colorPrimary: '#FF69B4', // 热粉色
|
||||
borderRadius: 1.5,
|
||||
},
|
||||
components: {
|
||||
Button: {
|
||||
colorPrimary: '#FFC0CB', // 深粉色
|
||||
},
|
||||
Layout: {
|
||||
headerBg: '#FFC0CB', // 粉色
|
||||
headerColor: '#000000', // 黑色文本
|
||||
backgroundColor: '#F8F8FF', // 幽灵白
|
||||
colorBgLayout: '#faf4f4',
|
||||
},
|
||||
Menu: {
|
||||
darkItemBg: '#FFC0CB', // 粉色
|
||||
horizontalItemSelectedBg: '#FF69B4',
|
||||
itemSelectedColor: '#ffffff',
|
||||
itemColor: '#000000', // 黑色文本
|
||||
colorText: 'rgba(0, 0, 0, 0.88)',
|
||||
activeBarBorderWidth: 0,
|
||||
},
|
||||
Drawer: {
|
||||
colorBgElevated: '#FFC0CB', // Drawer 背景色
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
export const greenTheme = {
|
||||
token: {
|
||||
colorPrimary: '#52c41a', // 绿色
|
||||
borderRadius: 1.5,
|
||||
},
|
||||
components: {
|
||||
Button: {
|
||||
colorPrimary: '#7cb305', // 橄榄绿
|
||||
},
|
||||
Layout: {
|
||||
headerBg: '#3f673f', // 深绿色
|
||||
headerColor: '#ffffff', // 白色文本
|
||||
backgroundColor: '#f6ffed',
|
||||
colorBgLayout: '#ebf8eb',
|
||||
},
|
||||
Menu: {
|
||||
darkItemBg: '#3f673f', // 深绿色
|
||||
horizontalItemSelectedBg: '#52c41a',
|
||||
itemSelectedColor: '#ffffff',
|
||||
itemColor: '#ffffff',
|
||||
colorText: 'rgba(255, 255, 255, 0.88)',
|
||||
activeBarBorderWidth: 0,
|
||||
},
|
||||
Drawer: {
|
||||
colorBgElevated: '#3f673f', // Drawer 背景色
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
export const themes = {
|
||||
default: defaultTheme,
|
||||
pink: pinkTheme,
|
||||
green: greenTheme,
|
||||
};
|
73
frontend-new/src/components/DlqMessageDetailViewDialog.jsx
Normal file
73
frontend-new/src/components/DlqMessageDetailViewDialog.jsx
Normal file
@@ -0,0 +1,73 @@
|
||||
/*
|
||||
* Licensed to the Apache Software Foundation (ASF) under one or more
|
||||
* contributor license agreements. See the NOTICE file distributed with
|
||||
* this work for additional information regarding copyright ownership.
|
||||
* The ASF licenses this file to You under the Apache License, Version 2.0
|
||||
* (the "License"); you may not use this file except in compliance with
|
||||
* the License. You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import React from 'react';
|
||||
import { Form, Input, Typography, Modal } from 'antd';
|
||||
import moment from 'moment';
|
||||
import { useLanguage } from '../i18n/LanguageContext'; // 根据实际路径调整
|
||||
|
||||
const { Text } = Typography;
|
||||
|
||||
const DlqMessageDetailViewDialog = ({ ngDialogData }) => {
|
||||
const { t } = useLanguage();
|
||||
|
||||
const messageView = ngDialogData?.messageView || {};
|
||||
|
||||
return (
|
||||
<div style={{ padding: '20px' }}>
|
||||
<Form layout="horizontal" labelCol={{ span: 4 }} wrapperCol={{ span: 20 }}>
|
||||
<Form.Item label="Message ID:">
|
||||
<Text strong>{messageView.msgId}</Text>
|
||||
</Form.Item>
|
||||
<Form.Item label="Topic:">
|
||||
<Text strong>{messageView.topic}</Text>
|
||||
</Form.Item>
|
||||
<Form.Item label="Properties:">
|
||||
<Input.TextArea
|
||||
value={typeof messageView.properties === 'object' ? JSON.stringify(messageView.properties, null, 2) : messageView.properties}
|
||||
style={{ minHeight: 100, resize: 'none' }}
|
||||
readOnly
|
||||
/>
|
||||
</Form.Item>
|
||||
<Form.Item label="ReconsumeTimes:">
|
||||
<Text strong>{messageView.reconsumeTimes}</Text>
|
||||
</Form.Item>
|
||||
<Form.Item label="Tag:">
|
||||
<Text strong>{messageView.properties?.TAGS}</Text>
|
||||
</Form.Item>
|
||||
<Form.Item label="Key:">
|
||||
<Text strong>{messageView.properties?.KEYS}</Text>
|
||||
</Form.Item>
|
||||
<Form.Item label="Storetime:">
|
||||
<Text strong>{moment(messageView.storeTimestamp).format('YYYY-MM-DD HH:mm:ss')}</Text>
|
||||
</Form.Item>
|
||||
<Form.Item label="StoreHost:">
|
||||
<Text strong>{messageView.storeHost}</Text>
|
||||
</Form.Item>
|
||||
<Form.Item label="Message body:">
|
||||
<Input.TextArea
|
||||
value={messageView.messageBody}
|
||||
style={{ minHeight: 100, resize: 'none' }}
|
||||
readOnly
|
||||
/>
|
||||
</Form.Item>
|
||||
</Form>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default DlqMessageDetailViewDialog;
|
208
frontend-new/src/components/MessageDetailViewDialog.jsx
Normal file
208
frontend-new/src/components/MessageDetailViewDialog.jsx
Normal file
@@ -0,0 +1,208 @@
|
||||
/*
|
||||
* Licensed to the Apache Software Foundation (ASF) under one or more
|
||||
* contributor license agreements. See the NOTICE file distributed with
|
||||
* this work for additional information regarding copyright ownership.
|
||||
* The ASF licenses this file to You under the Apache License, Version 2.0
|
||||
* (the "License"); you may not use this file except in compliance with
|
||||
* the License. You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import React from 'react';
|
||||
import { Modal, Button, Typography, Descriptions, Tag, Spin, notification } from 'antd';
|
||||
import moment from 'moment';
|
||||
import { ExclamationCircleOutlined, SyncOutlined } from '@ant-design/icons';
|
||||
import { useLanguage } from '../i18n/LanguageContext';
|
||||
import { remoteApi } from '../api/remoteApi/remoteApi'; // 确保这个路径正确
|
||||
|
||||
const { Text, Paragraph } = Typography;
|
||||
|
||||
const MessageDetailViewDialog = ({ visible, onCancel, messageId, topic, onResendMessage }) => {
|
||||
const { t } = useLanguage();
|
||||
const [loading, setLoading] = React.useState(true);
|
||||
const [messageDetail, setMessageDetail] = React.useState(null);
|
||||
const [error, setError] = React.useState(null);
|
||||
|
||||
React.useEffect(() => {
|
||||
const fetchMessageDetail = async () => {
|
||||
// 只有当 visible 为 true 且 messageId 和 topic 存在时才进行数据请求
|
||||
if (!visible || !messageId || !topic) {
|
||||
// 如果 Modal 不可见或者必要参数缺失,则不加载数据
|
||||
setMessageDetail(null); // 清空旧数据
|
||||
setError(null); // 清空错误信息
|
||||
setLoading(false); // 停止加载状态
|
||||
return;
|
||||
}
|
||||
|
||||
setLoading(true);
|
||||
setError(null); // 在每次新的请求前清除之前的错误
|
||||
try {
|
||||
const resp = await remoteApi.viewMessage(messageId, topic);
|
||||
if (resp.status === 0) {
|
||||
setMessageDetail(resp.data);
|
||||
} else {
|
||||
const errorMessage = resp.errMsg || t.FETCH_MESSAGE_DETAIL_FAILED;
|
||||
setError(errorMessage);
|
||||
notification.error({
|
||||
message: t.ERROR,
|
||||
description: errorMessage,
|
||||
});
|
||||
}
|
||||
} catch (err) {
|
||||
const errorMessage = t.FETCH_MESSAGE_DETAIL_FAILED;
|
||||
setError(errorMessage);
|
||||
notification.error({
|
||||
message: t.ERROR,
|
||||
description: errorMessage,
|
||||
});
|
||||
console.error("Error fetching message detail:", err);
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
fetchMessageDetail();
|
||||
}, [visible, messageId, topic, t]); // 依赖项中添加 visible,确保在 Modal 显示时触发
|
||||
|
||||
|
||||
// handleShowExceptionDesc 方法不再需要,因为我们直接使用 Paragraph 的 ellipsis
|
||||
|
||||
return (
|
||||
<Modal
|
||||
title={t.MESSAGE_DETAIL}
|
||||
open={visible} // Ant Design 5.x 版本中,visible 属性已更名为 open
|
||||
onCancel={onCancel}
|
||||
footer={[
|
||||
<Button key="close" onClick={onCancel}>
|
||||
{t.CLOSE}
|
||||
</Button>,
|
||||
]}
|
||||
width={900}
|
||||
destroyOnHidden={true} // 使用新的 destroyOnHidden 替代 destroyOnClose
|
||||
>
|
||||
<Spin spinning={loading} tip={t.LOADING}>
|
||||
{error && (
|
||||
<Paragraph type="danger" style={{ textAlign: 'center' }}>
|
||||
{error}
|
||||
</Paragraph>
|
||||
)}
|
||||
{messageDetail ? ( // 确保 messageDetail 存在时才渲染内容
|
||||
<>
|
||||
{/* 消息信息部分 */}
|
||||
<Descriptions title={<Text strong>{t.MESSAGE_INFO}</Text>} bordered column={2} size="small" style={{ marginBottom: 20 }}>
|
||||
<Descriptions.Item label="Topic" span={2}><Text 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="StoreTime">
|
||||
{moment(messageDetail.messageView.storeTimestamp).format("YYYY-MM-DD HH:mm:ss")}
|
||||
</Descriptions.Item>
|
||||
<Descriptions.Item label="BornTime">
|
||||
{moment(messageDetail.messageView.bornTimestamp).format("YYYY-MM-DD HH:mm:ss")}
|
||||
</Descriptions.Item>
|
||||
<Descriptions.Item label="Queue ID">{messageDetail.messageView.queueId}</Descriptions.Item>
|
||||
<Descriptions.Item label="Queue Offset">{messageDetail.messageView.queueOffset}</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="SysFlag">{messageDetail.messageView.sysFlag}</Descriptions.Item>
|
||||
<Descriptions.Item label="Flag">{messageDetail.messageView.flag}</Descriptions.Item>
|
||||
<Descriptions.Item label="PreparedTransactionOffset">{messageDetail.messageView.preparedTransactionOffset}</Descriptions.Item>
|
||||
</Descriptions>
|
||||
|
||||
{/* 消息属性部分 */}
|
||||
{Object.keys(messageDetail.messageView.properties).length > 0 && (
|
||||
<Descriptions title={<Text strong>{t.MESSAGE_PROPERTIES}</Text>} bordered column={1} size="small" style={{ marginBottom: 20 }}>
|
||||
{Object.entries(messageDetail.messageView.properties).map(([key, value]) => (
|
||||
<Descriptions.Item label={key} key={key}><Text copyable>{value}</Text></Descriptions.Item>
|
||||
))}
|
||||
</Descriptions>
|
||||
)}
|
||||
|
||||
{/* 消息体部分 */}
|
||||
<Descriptions title={<Text strong>{t.MESSAGE_BODY}</Text>} bordered column={1} size="small" style={{ marginBottom: 20 }}>
|
||||
<Descriptions.Item>
|
||||
<Paragraph
|
||||
copyable
|
||||
ellipsis={{
|
||||
rows: 5,
|
||||
expandable: true,
|
||||
symbol: t.SHOW_ALL_CONTENT,
|
||||
}}
|
||||
>
|
||||
{messageDetail.messageView.messageBody}
|
||||
</Paragraph>
|
||||
</Descriptions.Item>
|
||||
</Descriptions>
|
||||
|
||||
{/* 消息轨迹列表部分 */}
|
||||
{messageDetail.messageTrackList && messageDetail.messageTrackList.length > 0 ? (
|
||||
<>
|
||||
<Text strong>{t.MESSAGE_TRACKING}</Text>
|
||||
<div style={{ marginTop: 10 }}>
|
||||
{messageDetail.messageTrackList.map((track, index) => (
|
||||
<Descriptions bordered column={1} size="small" key={index} style={{ marginBottom: 15 }}>
|
||||
<Descriptions.Item label={t.CONSUMER_GROUP}>
|
||||
{track.consumerGroup}
|
||||
</Descriptions.Item>
|
||||
<Descriptions.Item label={t.TRACK_TYPE}>
|
||||
<Tag color={
|
||||
track.trackType === 'CONSUMED_SOME_TIME_OK' ? 'success' :
|
||||
track.trackType === 'NOT_ONLINE' ? 'default' :
|
||||
track.trackType === 'PULL_SUCCESS' ? 'processing' :
|
||||
track.trackType === 'NO_MATCHED_CONSUMER' ? 'warning' :
|
||||
'error'
|
||||
}>
|
||||
{track.trackType}
|
||||
</Tag>
|
||||
</Descriptions.Item>
|
||||
<Descriptions.Item label={t.OPERATION}>
|
||||
<Button
|
||||
icon={<SyncOutlined />}
|
||||
onClick={() => onResendMessage(messageDetail.messageView, track.consumerGroup)}
|
||||
size="small"
|
||||
style={{ marginRight: 8 }}
|
||||
>
|
||||
{t.RESEND_MESSAGE}
|
||||
</Button>
|
||||
{/* 移除“查看异常”按钮,因为现在直接在下方展示可展开内容 */}
|
||||
</Descriptions.Item>
|
||||
{track.exceptionDesc && (
|
||||
<Descriptions.Item label={t.EXCEPTION_SUMMARY}>
|
||||
{/* 异常信息截断显示,点击“查看更多”可展开 */}
|
||||
<Paragraph
|
||||
ellipsis={{
|
||||
rows: 2, // 默认显示2行
|
||||
expandable: true,
|
||||
symbol: <Text style={{ color: '#1890ff', cursor: 'pointer' }}>{t.READ_MORE}</Text>, // 蓝色展开文本
|
||||
}}
|
||||
>
|
||||
{track.exceptionDesc}
|
||||
</Paragraph>
|
||||
</Descriptions.Item>
|
||||
)}
|
||||
</Descriptions>
|
||||
))}
|
||||
</div>
|
||||
</>
|
||||
) : (
|
||||
<Paragraph>{t.NO_TRACKING_INFO}</Paragraph>
|
||||
)}
|
||||
</>
|
||||
) : (
|
||||
// 当 messageDetail 为 null 时,可以显示一个占位符或者不显示内容
|
||||
!loading && !error && <Paragraph style={{ textAlign: 'center' }}>{t.NO_MESSAGE_DETAIL_AVAILABLE}</Paragraph>
|
||||
)}
|
||||
</Spin>
|
||||
</Modal>
|
||||
);
|
||||
};
|
||||
|
||||
export default MessageDetailViewDialog;
|
470
frontend-new/src/components/MessageTraceDetailViewDialog.jsx
Normal file
470
frontend-new/src/components/MessageTraceDetailViewDialog.jsx
Normal file
@@ -0,0 +1,470 @@
|
||||
/*
|
||||
* Licensed to the Apache Software Foundation (ASF) under one or more
|
||||
* contributor license agreements. See the NOTICE file distributed with
|
||||
* this work for additional information regarding copyright ownership.
|
||||
* The ASF licenses this file to You under the Apache License, Version 2.0
|
||||
* (the "License"); you may not use this file except in compliance with
|
||||
* the License. You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import React, { useEffect, useRef } from 'react';
|
||||
import { Form, Input, Typography, Collapse, Table, Tag } from 'antd';
|
||||
import moment from 'moment';
|
||||
import { useLanguage } from '../i18n/LanguageContext';
|
||||
import Paragraph from "antd/es/skeleton/Paragraph";
|
||||
import * as echarts from 'echarts'; // Import ECharts
|
||||
|
||||
const { Text } = Typography;
|
||||
const { Panel } = Collapse;
|
||||
|
||||
// Constants for styling and formatting, derived from the example
|
||||
const SUCCESS_COLOR = '#75d874';
|
||||
const ERROR_COLOR = 'red';
|
||||
const UNKNOWN_COLOR = 'yellow';
|
||||
const TRANSACTION_COMMIT_COLOR = SUCCESS_COLOR;
|
||||
const TRANSACTION_ROLLBACK_COLOR = ERROR_COLOR;
|
||||
const TRANSACTION_UNKNOWN_COLOR = 'grey';
|
||||
const TIME_FORMAT_PATTERN = "YYYY-MM-DD HH:mm:ss.SSS";
|
||||
const DEFAULT_DISPLAY_DURATION = 10 * 1000;
|
||||
const TRANSACTION_CHECK_COST_TIME = 50; // transactionTraceNode do not have costTime, assume it cost 50ms
|
||||
|
||||
const MessageTraceDetailViewDialog = ({ ngDialogData }) => {
|
||||
const { t } = useLanguage();
|
||||
const messageTraceGraphRef = useRef(null);
|
||||
|
||||
const producerNode = ngDialogData?.producerNode;
|
||||
const subscriptionNodeList = ngDialogData?.subscriptionNodeList || [];
|
||||
const messageTraceViews = ngDialogData?.messageTraceViews || []; // This data structure seems redundant for the Gantt chart but can be used for extra tooltip details.
|
||||
|
||||
useEffect(() => {
|
||||
if (messageTraceGraphRef.current && ngDialogData) {
|
||||
const chart = echarts.init(messageTraceGraphRef.current);
|
||||
|
||||
let data = [];
|
||||
let dataZoomEnd = 100;
|
||||
let startTime = Number.MAX_VALUE;
|
||||
let endTime = 0;
|
||||
let messageGroups = []; // This will be our Y-axis categories
|
||||
|
||||
if (producerNode) {
|
||||
startTime = +producerNode.traceNode.beginTimestamp;
|
||||
endTime = +producerNode.traceNode.endTimestamp;
|
||||
}
|
||||
|
||||
// Helper functions from the provided example
|
||||
function buildNodeColor(traceNode) {
|
||||
if (traceNode.transactionState != null) {
|
||||
switch (traceNode.transactionState) {
|
||||
case 'COMMIT_MESSAGE':
|
||||
return TRANSACTION_COMMIT_COLOR;
|
||||
case 'ROLLBACK_MESSAGE':
|
||||
return TRANSACTION_ROLLBACK_COLOR;
|
||||
case 'UNKNOW':
|
||||
return TRANSACTION_UNKNOWN_COLOR;
|
||||
default:
|
||||
return ERROR_COLOR;
|
||||
}
|
||||
}
|
||||
switch (traceNode.status) {
|
||||
case 'FAILED': // Changed 'failed' to 'FAILED' to match backend typically
|
||||
return ERROR_COLOR;
|
||||
case 'UNKNOWN': // Changed 'unknown' to 'UNKNOWN'
|
||||
return UNKNOWN_COLOR;
|
||||
default:
|
||||
return SUCCESS_COLOR;
|
||||
}
|
||||
}
|
||||
|
||||
function formatXAxisTime(value) {
|
||||
let duration = Math.max(0, value - startTime);
|
||||
if (duration < 1000)
|
||||
return timeFormat(duration, 'ms');
|
||||
duration /= 1000;
|
||||
if (duration < 60)
|
||||
return timeFormat(duration, 's');
|
||||
duration /= 60;
|
||||
if (duration < 60)
|
||||
return timeFormat(duration, 'min');
|
||||
duration /= 60;
|
||||
return timeFormat(duration, 'h');
|
||||
}
|
||||
|
||||
function timeFormat(duration, unit) {
|
||||
return duration.toFixed(2) + unit;
|
||||
}
|
||||
|
||||
function buildTraceInfo(itemName, itemValue) {
|
||||
if (itemValue) {
|
||||
return `${itemName}: ${itemValue}<br />`
|
||||
}
|
||||
return "";
|
||||
}
|
||||
|
||||
function formatCostTimeStr(costTime) {
|
||||
if (costTime < 0) {
|
||||
return "";
|
||||
}
|
||||
let costTimeStr = costTime;
|
||||
if (costTime === 0) {
|
||||
costTimeStr = '<1'
|
||||
}
|
||||
return `${costTimeStr}ms`;
|
||||
}
|
||||
|
||||
function buildCostTimeInfo(costTime) {
|
||||
if (costTime < 0) {
|
||||
return "";
|
||||
}
|
||||
return `Cost Time: ${formatCostTimeStr(costTime)}<br/>`
|
||||
}
|
||||
function buildTimeStamp(timestamp) {
|
||||
if (timestamp < 0) {
|
||||
return 'N/A';
|
||||
}
|
||||
return moment(timestamp).format(TIME_FORMAT_PATTERN);
|
||||
}
|
||||
|
||||
function formatNodeToolTip(params) {
|
||||
let traceNode = params.data.traceData.traceNode;
|
||||
return `
|
||||
${buildCostTimeInfo(traceNode.costTime)}
|
||||
Status: ${traceNode.status}<br />
|
||||
${buildTraceInfo('Begin Timestamp', buildTimeStamp(traceNode.beginTimestamp))}
|
||||
${buildTraceInfo('End Timestamp', buildTimeStamp(traceNode.endTimestamp))}
|
||||
Client Host: ${traceNode.clientHost}<br />
|
||||
Store Host: ${traceNode.storeHost}<br />
|
||||
Retry Times: ${traceNode.retryTimes < 0 ? 'N/A' : traceNode.retryTimes}<br />
|
||||
${buildTraceInfo('Message Type', traceNode.msgType)}
|
||||
${buildTraceInfo('Transaction ID', traceNode.transactionId)}
|
||||
${buildTraceInfo('Transaction State', traceNode.transactionState)}
|
||||
${buildTraceInfo('From Transaction Check', traceNode.fromTransactionCheck)}
|
||||
`;
|
||||
}
|
||||
|
||||
function calcGraphTimestamp(timestamp, relativeTimeStamp, duration, addDuration) {
|
||||
if (timestamp > 0) {
|
||||
return timestamp;
|
||||
}
|
||||
if (duration < 0) {
|
||||
return relativeTimeStamp;
|
||||
}
|
||||
return addDuration ? relativeTimeStamp + duration : relativeTimeStamp - duration;
|
||||
}
|
||||
|
||||
function addTraceData(traceNode, index, groupName) {
|
||||
if (traceNode.beginTimestamp < 0 && traceNode.endTimestamp < 0) {
|
||||
return;
|
||||
}
|
||||
let beginTimestamp = calcGraphTimestamp(traceNode.beginTimestamp, traceNode.endTimestamp, traceNode.costTime, false);
|
||||
let endTimestamp = calcGraphTimestamp(traceNode.endTimestamp, traceNode.beginTimestamp, traceNode.costTime, true);
|
||||
if (endTimestamp === beginTimestamp) {
|
||||
endTimestamp = beginTimestamp + 1; // Ensure a minimum duration for visualization
|
||||
}
|
||||
data.push({
|
||||
name: groupName, // To display group name in tooltip or for internal reference
|
||||
value: [
|
||||
index,
|
||||
beginTimestamp,
|
||||
endTimestamp,
|
||||
traceNode.costTime
|
||||
],
|
||||
itemStyle: {
|
||||
normal: {
|
||||
color: buildNodeColor(traceNode),
|
||||
opacity: 1
|
||||
}
|
||||
},
|
||||
traceData: {
|
||||
traceNode: traceNode
|
||||
}
|
||||
});
|
||||
startTime = Math.min(startTime, beginTimestamp);
|
||||
endTime = Math.max(endTime, endTimestamp);
|
||||
}
|
||||
|
||||
// Populate data for the Gantt chart
|
||||
subscriptionNodeList.forEach(item => {
|
||||
messageGroups.push(item.subscriptionGroup);
|
||||
});
|
||||
|
||||
subscriptionNodeList.forEach((subscriptionNode, index) => {
|
||||
subscriptionNode.consumeNodeList.forEach(traceNode => addTraceData(traceNode, index, subscriptionNode.subscriptionGroup));
|
||||
});
|
||||
|
||||
if (producerNode) {
|
||||
messageGroups.push(producerNode.groupName);
|
||||
let producerNodeIndex = messageGroups.length - 1;
|
||||
addTraceData(producerNode.traceNode, producerNodeIndex, producerNode.groupName);
|
||||
producerNode.transactionNodeList.forEach(transactionNode => {
|
||||
transactionNode.beginTimestamp = Math.max(producerNode.traceNode.endTimestamp,
|
||||
transactionNode.endTimestamp - TRANSACTION_CHECK_COST_TIME);
|
||||
addTraceData(transactionNode, producerNodeIndex, producerNode.groupName);
|
||||
endTime = Math.max(endTime, transactionNode.endTimestamp);
|
||||
});
|
||||
}
|
||||
|
||||
let totalDuration = endTime - startTime;
|
||||
if (totalDuration > DEFAULT_DISPLAY_DURATION) {
|
||||
dataZoomEnd = (DEFAULT_DISPLAY_DURATION / totalDuration) * 100;
|
||||
}
|
||||
|
||||
function renderItem(params, api) {
|
||||
let messageGroupIndex = api.value(0); // Y-axis index
|
||||
let start = api.coord([api.value(1), messageGroupIndex]); // X-axis start time, Y-axis group index
|
||||
let end = api.coord([api.value(2), messageGroupIndex]); // X-axis end time, Y-axis group index
|
||||
let height = api.size([0, 1])[1] * 0.6; // Height of the bar
|
||||
|
||||
let rectShape = echarts.graphic.clipRectByRect({
|
||||
x: start[0],
|
||||
y: start[1] - height / 2,
|
||||
width: Math.max(end[0] - start[0], 1), // Ensure minimum width
|
||||
height: height
|
||||
}, {
|
||||
x: params.coordSys.x,
|
||||
y: params.coordSys.y,
|
||||
width: params.coordSys.width,
|
||||
height: params.coordSys.height
|
||||
});
|
||||
|
||||
return rectShape && {
|
||||
type: 'rect',
|
||||
transition: ['shape'],
|
||||
shape: rectShape,
|
||||
style: api.style({
|
||||
text: formatCostTimeStr(api.value(3)), // Display cost time on the bar
|
||||
textFill: '#000',
|
||||
textAlign: 'right'
|
||||
})
|
||||
};
|
||||
}
|
||||
|
||||
const option = {
|
||||
tooltip: {
|
||||
formatter: function (params) {
|
||||
return formatNodeToolTip(params);
|
||||
}
|
||||
},
|
||||
title: {
|
||||
text: producerNode ? `Message Trace: ${producerNode.topic}` : "Message Trace",
|
||||
left: 'center'
|
||||
},
|
||||
dataZoom: [
|
||||
{
|
||||
type: 'slider',
|
||||
filterMode: 'weakFilter',
|
||||
showDataShadow: false,
|
||||
top: 'bottom', // Position at the bottom
|
||||
start: 0,
|
||||
end: dataZoomEnd,
|
||||
labelFormatter: function (value) {
|
||||
return formatXAxisTime(value + startTime); // Adjust label to show relative time from start
|
||||
}
|
||||
},
|
||||
{
|
||||
type: 'inside',
|
||||
filterMode: 'weakFilter'
|
||||
}
|
||||
],
|
||||
grid: {
|
||||
height: 300,
|
||||
left: '10%', // Adjust left margin for Y-axis labels
|
||||
right: '10%'
|
||||
},
|
||||
xAxis: {
|
||||
min: startTime,
|
||||
scale: true,
|
||||
axisLabel: {
|
||||
formatter: function (value) {
|
||||
return formatXAxisTime(value);
|
||||
}
|
||||
}
|
||||
},
|
||||
yAxis: {
|
||||
data: messageGroups, // Use group names as Y-axis categories
|
||||
axisLabel: {
|
||||
formatter: function (value, index) {
|
||||
// Display the group name on the Y-axis
|
||||
return value;
|
||||
}
|
||||
}
|
||||
},
|
||||
series: [
|
||||
{
|
||||
type: 'custom',
|
||||
renderItem: renderItem,
|
||||
encode: {
|
||||
x: [1, 2], // Use beginTimestamp and endTimestamp for X-axis
|
||||
y: 0 // Use the index for Y-axis category
|
||||
},
|
||||
data: data
|
||||
}
|
||||
]
|
||||
};
|
||||
|
||||
chart.setOption(option);
|
||||
|
||||
const resizeChart = () => chart.resize();
|
||||
window.addEventListener('resize', resizeChart);
|
||||
|
||||
return () => {
|
||||
window.removeEventListener('resize', resizeChart);
|
||||
chart.dispose();
|
||||
};
|
||||
}
|
||||
}, [ngDialogData, t]); // Add t as a dependency for the useEffect hook
|
||||
|
||||
// ... (rest of your existing component code)
|
||||
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.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 = [
|
||||
{ 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.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 (
|
||||
<div style={{ padding: '20px', backgroundColor: '#f0f2f5' }}>
|
||||
<div style={{ marginBottom: '20px', borderRadius: '8px', overflow: 'hidden', boxShadow: '0 4px 12px rgba(0, 0, 0, 0.08)' }}>
|
||||
<Collapse defaultActiveKey={['messageTraceGraph']} expandIconPosition="right">
|
||||
<Panel header={<Typography.Title level={3} style={{ 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 */}
|
||||
{(!producerNode && subscriptionNodeList.length === 0) && (
|
||||
<Text type="secondary" style={{ display: 'block', textAlign: 'center', marginTop: '150px' }}>{t.TRACE_GRAPH_PLACEHOLDER}</Text>
|
||||
)}
|
||||
</div>
|
||||
</Panel>
|
||||
</Collapse>
|
||||
</div>
|
||||
|
||||
<div style={{ marginBottom: '20px', borderRadius: '8px', overflow: 'hidden', boxShadow: '0 4px 12px rgba(0, 0, 0, 0.08)' }}>
|
||||
<Collapse defaultActiveKey={['sendMessageTrace']} expandIconPosition="right">
|
||||
<Panel header={<Typography.Title level={3} style={{ margin: 0, color: '#333' }}>{t.SEND_MESSAGE_TRACE}</Typography.Title>} key="sendMessageTrace">
|
||||
{!producerNode ? (
|
||||
<Paragraph style={{ padding: '16px', textAlign: 'center', color: '#666' }}>{t.NO_PRODUCER_TRACE_DATA}</Paragraph>
|
||||
) : (
|
||||
<div style={{ padding: '16px', backgroundColor: '#fff' }}>
|
||||
<Typography.Title level={4} style={{ marginBottom: '20px' }}>
|
||||
{t.SEND_MESSAGE_INFO} : ( {t.MESSAGE_ID} <Text strong copyable>{producerNode.msgId}</Text> )
|
||||
</Typography.Title>
|
||||
<Form layout="vertical" colon={false}>
|
||||
<div style={{ display: 'grid', gridTemplateColumns: 'repeat(auto-fill, minmax(280px, 1fr))', gap: '16px' }}>
|
||||
<Form.Item label={<Text strong>{t.TOPIC}</Text>}>
|
||||
<Input value={producerNode.topic} readOnly />
|
||||
</Form.Item>
|
||||
<Form.Item label={<Text strong>{t.PRODUCER_GROUP}</Text>}>
|
||||
<Input value={producerNode.groupName} readOnly />
|
||||
</Form.Item>
|
||||
<Form.Item label={<Text strong>{t.MESSAGE_KEY}</Text>}>
|
||||
<Input value={producerNode.keys} readOnly />
|
||||
</Form.Item>
|
||||
<Form.Item label={<Text strong>{t.TAG}</Text>}>
|
||||
<Input value={producerNode.tags} readOnly />
|
||||
</Form.Item>
|
||||
|
||||
<Form.Item label={<Text strong>{t.BEGIN_TIMESTAMP}</Text>}>
|
||||
<Input value={moment(producerNode.traceNode.beginTimestamp).format('YYYY-MM-DD HH:mm:ss.SSS')} readOnly />
|
||||
</Form.Item>
|
||||
<Form.Item label={<Text strong>{t.END_TIMESTAMP}</Text>}>
|
||||
<Input value={moment(producerNode.traceNode.endTimestamp).format('YYYY-MM-DD HH:mm:ss.SSS')} readOnly />
|
||||
</Form.Item>
|
||||
<Form.Item label={<Text strong>{t.COST_TIME}</Text>}>
|
||||
<Input value={`${producerNode.traceNode.costTime === 0 ? '<1' : producerNode.traceNode.costTime}ms`} readOnly />
|
||||
</Form.Item>
|
||||
<Form.Item label={<Text strong>{t.MSG_TYPE}</Text>}>
|
||||
<Input value={producerNode.traceNode.msgType} readOnly />
|
||||
</Form.Item>
|
||||
|
||||
<Form.Item label={<Text strong>{t.CLIENT_HOST}</Text>}>
|
||||
<Input value={producerNode.traceNode.clientHost} readOnly />
|
||||
</Form.Item>
|
||||
<Form.Item label={<Text strong>{t.STORE_HOST}</Text>}>
|
||||
<Input value={producerNode.traceNode.storeHost} readOnly />
|
||||
</Form.Item>
|
||||
<Form.Item label={<Text strong>{t.RETRY_TIMES}</Text>}>
|
||||
<Input value={producerNode.traceNode.retryTimes} readOnly />
|
||||
</Form.Item>
|
||||
<Form.Item label={<Text strong>{t.OFFSET_MSG_ID}</Text>}>
|
||||
<Input value={producerNode.offSetMsgId} readOnly />
|
||||
</Form.Item>
|
||||
</div>
|
||||
</Form>
|
||||
|
||||
{producerNode.transactionNodeList && producerNode.transactionNodeList.length > 0 && (
|
||||
<div style={{ marginTop: '30px' }}>
|
||||
<Typography.Title level={4} style={{ marginBottom: '15px' }}>{t.CHECK_TRANSACTION_INFO}:</Typography.Title>
|
||||
<Table
|
||||
columns={transactionColumns}
|
||||
dataSource={producerNode.transactionNodeList}
|
||||
rowKey={(record, index) => `transaction_${index}`}
|
||||
bordered
|
||||
pagination={false}
|
||||
size="middle"
|
||||
scroll={{ x: 'max-content' }}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</Panel>
|
||||
</Collapse>
|
||||
</div>
|
||||
|
||||
<div style={{ borderRadius: '8px', overflow: 'hidden', boxShadow: '0 4px 12px rgba(0, 0, 0, 0.08)' }}>
|
||||
<Collapse defaultActiveKey={['consumeMessageTrace']} expandIconPosition="right">
|
||||
<Panel header={<Typography.Title level={3} style={{ margin: 0, color: '#333' }}>{t.CONSUME_MESSAGE_TRACE}</Typography.Title>} key="consumeMessageTrace">
|
||||
{subscriptionNodeList.length === 0 ? (
|
||||
<Paragraph style={{ padding: '16px', textAlign: 'center', color: '#666' }}>{t.NO_CONSUMER_TRACE_DATA}</Paragraph>
|
||||
) : (
|
||||
<div style={{ padding: '16px', backgroundColor: '#fff' }}>
|
||||
{subscriptionNodeList.map(subscriptionNode => (
|
||||
<Collapse
|
||||
key={subscriptionNode.subscriptionGroup}
|
||||
style={{ marginBottom: '10px', border: '1px solid #e0e0e0', borderRadius: '4px' }}
|
||||
defaultActiveKey={[subscriptionNode.subscriptionGroup]}
|
||||
ghost
|
||||
>
|
||||
<Panel
|
||||
header={<Typography.Title level={4} style={{ margin: 0 }}>{t.SUBSCRIPTION_GROUP}: <Text strong>{subscriptionNode.subscriptionGroup}</Text></Typography.Title>}
|
||||
key={subscriptionNode.subscriptionGroup}
|
||||
>
|
||||
<Table
|
||||
columns={consumeColumns}
|
||||
dataSource={subscriptionNode.consumeNodeList}
|
||||
rowKey={(record, index) => `${subscriptionNode.subscriptionGroup}_${index}`}
|
||||
bordered
|
||||
pagination={false}
|
||||
size="middle"
|
||||
scroll={{ x: 'max-content' }}
|
||||
/>
|
||||
</Panel>
|
||||
</Collapse>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</Panel>
|
||||
</Collapse>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default MessageTraceDetailViewDialog;
|
211
frontend-new/src/components/Navbar.jsx
Normal file
211
frontend-new/src/components/Navbar.jsx
Normal file
@@ -0,0 +1,211 @@
|
||||
/*
|
||||
* Licensed to the Apache Software Foundation (ASF) under one or more
|
||||
* contributor license agreements. See the NOTICE file distributed with
|
||||
* this work for additional information regarding copyright ownership.
|
||||
* The ASF licenses this file to You under the Apache License, Version 2.0
|
||||
* (the "License"); you may not use this file except in compliance with
|
||||
* the License. You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import React, {useEffect, useState} from 'react';
|
||||
import { Layout, Menu, Dropdown, Button, Drawer, Grid, Space } from 'antd';
|
||||
import {GlobalOutlined, DownOutlined, UserOutlined, MenuOutlined, BgColorsOutlined} from '@ant-design/icons';
|
||||
import { useLocation, useNavigate } from 'react-router-dom';
|
||||
import { useLanguage } from '../i18n/LanguageContext';
|
||||
import {useTheme} from "../store/context/ThemeContext";
|
||||
import {remoteApi} from "../api/remoteApi/remoteApi";
|
||||
|
||||
const { Header } = Layout;
|
||||
const { useBreakpoint } = Grid; // Used to determine screen breakpoints
|
||||
|
||||
const Navbar = ({ rmqVersion = true, showAcl = true}) => {
|
||||
const location = useLocation();
|
||||
const navigate = useNavigate();
|
||||
const { lang, setLang, t } = useLanguage();
|
||||
const screens = useBreakpoint(); // Get current screen size breakpoints
|
||||
const { currentThemeName, setCurrentThemeName } = useTheme();
|
||||
const [userName, setUserName] = useState(null);
|
||||
const [drawerVisible, setDrawerVisible] = useState(false); // Controls drawer visibility
|
||||
|
||||
// Get selected menu item key based on current route path
|
||||
const getPath = () => location.pathname.replace('/', '');
|
||||
|
||||
const handleMenuClick = ({ key }) => {
|
||||
navigate(`/${key}`);
|
||||
setDrawerVisible(false); // Close drawer after clicking a menu item
|
||||
};
|
||||
|
||||
const onLogout = () => {
|
||||
remoteApi.logout().then(res => {
|
||||
if (res.status === 0) {
|
||||
window.localStorage.removeItem("username");
|
||||
window.localStorage.removeItem("userRole");
|
||||
window.localStorage.removeItem("token");
|
||||
window.localStorage.removeItem("rmqVersion");
|
||||
navigate('/login');
|
||||
} else {
|
||||
console.error('Logout failed:', res.message)
|
||||
navigate('/login');
|
||||
}
|
||||
})
|
||||
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
const storedUsername = window.localStorage.getItem("username");
|
||||
if (storedUsername) {
|
||||
setUserName(storedUsername);
|
||||
}else {
|
||||
setUserName(null);
|
||||
}
|
||||
}, []);
|
||||
|
||||
const langMenu = (
|
||||
<Menu onClick={({ key }) => setLang(key)}>
|
||||
<Menu.Item key="en">{t.ENGLISH}</Menu.Item>
|
||||
<Menu.Item key="zh">{t.CHINESE}</Menu.Item>
|
||||
</Menu>
|
||||
);
|
||||
|
||||
const userMenu = (
|
||||
<Menu>
|
||||
<Menu.Item key="logout" onClick={onLogout}>{t.LOGOUT}</Menu.Item>
|
||||
</Menu>
|
||||
);
|
||||
|
||||
const themeMenu = (
|
||||
<Menu onClick={({ key }) => setCurrentThemeName(key)}>
|
||||
<Menu.Item key="default">{t.BLUE} ({t.DEFAULT})</Menu.Item>
|
||||
<Menu.Item key="pink">{t.PINK}</Menu.Item>
|
||||
<Menu.Item key="green">{t.GREEN}</Menu.Item>
|
||||
</Menu>
|
||||
);
|
||||
|
||||
|
||||
// Menu item configuration
|
||||
const menuItems = [
|
||||
{ key: 'ops', label: t.OPS },
|
||||
...(rmqVersion ? [{ key: 'proxy', label: t.PROXY }] : []),
|
||||
{ key: '', label: t.DASHBOARD }, // Dashboard corresponds to root path
|
||||
{ key: 'cluster', label: t.CLUSTER },
|
||||
{ key: 'topic', label: t.TOPIC },
|
||||
{ key: 'consumer', label: t.CONSUMER },
|
||||
{ key: 'producer', label: t.PRODUCER },
|
||||
{ key: 'message', label: t.MESSAGE },
|
||||
{ key: 'dlqMessage', label: t.DLQ_MESSAGE },
|
||||
{ key: 'messageTrace', label: t.MESSAGETRACE },
|
||||
...(showAcl ? [{ key: 'acl', label: t.WHITE_LIST }] : []),
|
||||
];
|
||||
|
||||
// Determine if it's a small screen (e.g., less than md)
|
||||
const isSmallScreen = !screens.md;
|
||||
// Determine if it's an extra small screen (e.g., less than sm)
|
||||
const isExtraSmallScreen = !screens.sm;
|
||||
|
||||
return (
|
||||
<Header
|
||||
className="navbar"
|
||||
style={{
|
||||
display: 'flex',
|
||||
justifyContent: 'space-between',
|
||||
alignItems: 'center',
|
||||
padding: isExtraSmallScreen ? '0 16px' : '0 24px', // Smaller padding on extra small screens
|
||||
}}
|
||||
>
|
||||
<div className="navbar-left" style={{ display: 'flex', alignItems: 'center' }}>
|
||||
<div
|
||||
style={{
|
||||
fontWeight: 'bold',
|
||||
marginRight: isSmallScreen ? '16px' : '24px', // Adjust margin on small screens
|
||||
whiteSpace: 'nowrap', // Prevent text wrapping
|
||||
flexShrink: 0, // Prevent shrinking in flex container
|
||||
color: 'white', // Title text color also set to white
|
||||
fontSize: isSmallScreen ? '14px' : '18px',
|
||||
}}
|
||||
>
|
||||
{t.TITLE}
|
||||
</div>
|
||||
|
||||
{!isSmallScreen && ( // Display full menu on large screens
|
||||
<Menu
|
||||
onClick={handleMenuClick}
|
||||
selectedKeys={[getPath()]}
|
||||
mode="horizontal"
|
||||
items={menuItems}
|
||||
theme="dark" // Use dark theme to match Header background
|
||||
style={{ flex: 1, minWidth: 0 }} // Allow menu items to adapt width
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<Space size={isExtraSmallScreen ? 8 : 16} > {/* Adjust spacing for buttons */}
|
||||
{/* Theme switch button */}
|
||||
<Dropdown overlay={themeMenu}>
|
||||
<Button icon={<BgColorsOutlined />} size="small">
|
||||
{!isExtraSmallScreen && `${t.TOPIC}: ${currentThemeName}`}
|
||||
<DownOutlined />
|
||||
</Button>
|
||||
</Dropdown>
|
||||
<Dropdown overlay={langMenu}>
|
||||
<Button icon={<GlobalOutlined />} size="small">
|
||||
{!isExtraSmallScreen && t.CHANGE_LANG} {/* Hide text on extra small screens */}
|
||||
<DownOutlined />
|
||||
</Button>
|
||||
</Dropdown>
|
||||
|
||||
{userName && (
|
||||
<Dropdown overlay={userMenu}>
|
||||
{/* 使用一个可点击的元素作为 Dropdown 的唯一子元素 */}
|
||||
<a onClick={e => e.preventDefault()} style={{ display: 'flex', alignItems: 'center' }}>
|
||||
<UserOutlined style={{ marginRight: 8 }} /> {/* 添加一些间距 */}
|
||||
{userName}
|
||||
<DownOutlined style={{ marginLeft: 8 }} />
|
||||
</a>
|
||||
</Dropdown>
|
||||
)}
|
||||
|
||||
{isSmallScreen && ( // Display hamburger icon on small screens
|
||||
<Button
|
||||
type="primary"
|
||||
icon={<MenuOutlined />}
|
||||
onClick={() => setDrawerVisible(true)}
|
||||
style={{ marginLeft: isExtraSmallScreen ? 8 : 16 }} // Adjust margin for hamburger icon
|
||||
/>
|
||||
)}
|
||||
</Space>
|
||||
|
||||
{/* Modify Drawer and Menu components here */}
|
||||
<Drawer
|
||||
// Default Drawer background color is white. If you need to change the Drawer's own background color, set it additionally
|
||||
// or set a dark background in bodyStyle, then let Menu override it
|
||||
title={t.MENU} // Drawer title
|
||||
placement="left" // Drawer pops out from the left
|
||||
onClose={() => setDrawerVisible(false)}
|
||||
open={drawerVisible}
|
||||
// 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.
|
||||
bodyStyle={{ padding: 0, backgroundColor: '#1c324a' }} // Set Drawer body background to dark
|
||||
width={200} // Set drawer width
|
||||
>
|
||||
<Menu
|
||||
onClick={handleMenuClick}
|
||||
selectedKeys={[getPath()]}
|
||||
mode="inline" // Use vertical menu in drawer
|
||||
items={menuItems}
|
||||
theme="dark"
|
||||
style={{ height: '100%', borderRight: 0 }} // Ensure menu fills the drawer
|
||||
/>
|
||||
</Drawer>
|
||||
</Header>
|
||||
);
|
||||
};
|
||||
|
||||
export default Navbar;
|
127
frontend-new/src/components/acl/ResourceInput.jsx
Normal file
127
frontend-new/src/components/acl/ResourceInput.jsx
Normal file
@@ -0,0 +1,127 @@
|
||||
/*
|
||||
* Licensed to the Apache Software Foundation (ASF) under one or more
|
||||
* contributor license agreements. See the NOTICE file distributed with
|
||||
* this work for additional information regarding copyright ownership.
|
||||
* The ASF licenses this file to You under the Apache License, Version 2.0
|
||||
* (the "License"); you may not use this file except in compliance with
|
||||
* the License. You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import { Input, Select, Tag, Space } from 'antd';
|
||||
import { PlusOutlined } from '@ant-design/icons';
|
||||
import React, { useState } from 'react';
|
||||
|
||||
const { Option } = Select;
|
||||
|
||||
// 资源类型枚举
|
||||
const resourceTypes = [
|
||||
{ value: 0, label: 'Unknown', prefix: 'UNKNOWN' },
|
||||
{ value: 1, label: 'Any', prefix: 'ANY' },
|
||||
{ value: 2, label: 'Cluster', prefix: 'CLUSTER' },
|
||||
{ value: 3, label: 'Namespace', prefix: 'NAMESPACE' },
|
||||
{ value: 4, label: 'Topic', prefix: 'TOPIC' },
|
||||
{ value: 5, label: 'Group', prefix: 'GROUP' },
|
||||
];
|
||||
|
||||
const ResourceInput = ({ value = [], onChange }) => {
|
||||
// 确保 value 始终是数组
|
||||
const safeValue = Array.isArray(value) ? value : [];
|
||||
|
||||
const [selectedType, setSelectedType] = useState(resourceTypes[0].prefix); // 默认选中第一个
|
||||
const [resourceName, setResourceName] = useState('');
|
||||
const [inputVisible, setInputVisible] = useState(false);
|
||||
const inputRef = React.useRef(null);
|
||||
|
||||
// 处理删除已添加的资源
|
||||
const handleClose = removedResource => {
|
||||
const newResources = safeValue.filter(resource => resource !== removedResource);
|
||||
onChange(newResources);
|
||||
};
|
||||
|
||||
// 显示输入框
|
||||
const showInput = () => {
|
||||
setInputVisible(true);
|
||||
setTimeout(() => {
|
||||
inputRef.current?.focus();
|
||||
}, 0);
|
||||
};
|
||||
|
||||
// 处理资源类型选择
|
||||
const handleTypeChange = type => {
|
||||
setSelectedType(type);
|
||||
};
|
||||
|
||||
// 处理资源名称输入
|
||||
const handleNameChange = e => {
|
||||
setResourceName(e.target.value);
|
||||
};
|
||||
|
||||
// 添加资源到列表
|
||||
const handleAddResource = () => {
|
||||
if (resourceName) {
|
||||
const fullResource = `${selectedType}:${resourceName}`;
|
||||
// 避免重复添加
|
||||
if (!safeValue.includes(fullResource)) {
|
||||
onChange([...safeValue, fullResource]);
|
||||
}
|
||||
setResourceName(''); // 清空输入
|
||||
setInputVisible(false); // 隐藏输入框
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<Space size={[0, 8]} wrap>
|
||||
{/* 显示已添加的资源标签 */}
|
||||
{safeValue.map(resource => ( // 使用 safeValue
|
||||
<Tag
|
||||
key={resource}
|
||||
closable
|
||||
onClose={() => handleClose(resource)}
|
||||
color="blue"
|
||||
>
|
||||
{resource}
|
||||
</Tag>
|
||||
))}
|
||||
|
||||
{/* 新增资源输入区域 */}
|
||||
{inputVisible ? (
|
||||
<Space>
|
||||
<Select
|
||||
value={selectedType}
|
||||
style={{ width: 120 }}
|
||||
onChange={handleTypeChange}
|
||||
>
|
||||
{resourceTypes.map(type => (
|
||||
<Option key={type.value} value={type.prefix}>
|
||||
{type.label}
|
||||
</Option>
|
||||
))}
|
||||
</Select>
|
||||
<Input
|
||||
ref={inputRef}
|
||||
style={{ width: 180 }}
|
||||
value={resourceName}
|
||||
onChange={handleNameChange}
|
||||
onPressEnter={handleAddResource}
|
||||
onBlur={handleAddResource} // 失去焦点也自动添加
|
||||
placeholder="请输入资源名称"
|
||||
/>
|
||||
</Space>
|
||||
) : (
|
||||
<Tag onClick={showInput} style={{ background: '#fff', borderStyle: 'dashed' }}>
|
||||
<PlusOutlined /> 添加资源
|
||||
</Tag>
|
||||
)}
|
||||
</Space>
|
||||
);
|
||||
};
|
||||
|
||||
export default ResourceInput;
|
101
frontend-new/src/components/acl/SubjectInput.jsx
Normal file
101
frontend-new/src/components/acl/SubjectInput.jsx
Normal file
@@ -0,0 +1,101 @@
|
||||
/*
|
||||
* Licensed to the Apache Software Foundation (ASF) under one or more
|
||||
* contributor license agreements. See the NOTICE file distributed with
|
||||
* this work for additional information regarding copyright ownership.
|
||||
* The ASF licenses this file to You under the Apache License, Version 2.0
|
||||
* (the "License"); you may not use this file except in compliance with
|
||||
* the License. You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import { Input, Select } from 'antd';
|
||||
import React, { useState, useEffect } from 'react';
|
||||
|
||||
const { Option } = Select;
|
||||
|
||||
// Subject 类型枚举
|
||||
const subjectTypes = [
|
||||
{ value: 'User', label: 'User' },
|
||||
];
|
||||
|
||||
const SubjectInput = ({ value, onChange, disabled }) => {
|
||||
// 解析传入的 value,将其拆分为 type 和 name
|
||||
const parseValue = (val) => {
|
||||
if (!val || typeof val !== 'string') {
|
||||
return { type: subjectTypes[0].value, name: '' }; // 默认值
|
||||
}
|
||||
const parts = val.split(':');
|
||||
if (parts.length === 2 && subjectTypes.some(t => t.value === parts[0])) {
|
||||
return { type: parts[0], name: parts[1] };
|
||||
}
|
||||
return { type: subjectTypes[0].value, name: val }; // 如果格式不匹配,将整个值作为 name,类型设为默认
|
||||
};
|
||||
|
||||
const [currentType, setCurrentType] = useState(() => parseValue(value).type);
|
||||
const [currentName, setCurrentName] = useState(() => parseValue(value).name);
|
||||
|
||||
// 当外部 value 变化时,更新内部状态
|
||||
useEffect(() => {
|
||||
const parsed = parseValue(value);
|
||||
setCurrentType(parsed.type);
|
||||
setCurrentName(parsed.name);
|
||||
}, [value]);
|
||||
|
||||
// 当类型或名称变化时,通知 Form.Item
|
||||
const triggerChange = (changedType, changedName) => {
|
||||
if (onChange) {
|
||||
// 只有当名称不为空时才组合,否则只返回类型或空字符串
|
||||
if (changedName) {
|
||||
onChange(`${changedType}:${changedName}`);
|
||||
} else if (changedType) { // 如果只选择了类型,但名称为空,则不组合
|
||||
onChange(''); // 或者根据需求返回 'User:' 等,但通常这种情况下不应该有值
|
||||
} else {
|
||||
onChange('');
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const onTypeChange = (newType) => {
|
||||
setCurrentType(newType);
|
||||
triggerChange(newType, currentName);
|
||||
};
|
||||
|
||||
const onNameChange = (e) => {
|
||||
const newName = e.target.value;
|
||||
setCurrentName(newName);
|
||||
triggerChange(currentType, newName);
|
||||
};
|
||||
|
||||
return (
|
||||
<Input.Group compact>
|
||||
<Select
|
||||
style={{ width: '30%' }}
|
||||
value={currentType}
|
||||
onChange={onTypeChange}
|
||||
disabled={disabled}
|
||||
>
|
||||
{subjectTypes.map(type => (
|
||||
<Option key={type.value} value={type.value}>
|
||||
{type.label}
|
||||
</Option>
|
||||
))}
|
||||
</Select>
|
||||
<Input
|
||||
style={{ width: '70%' }}
|
||||
value={currentName}
|
||||
onChange={onNameChange}
|
||||
placeholder="请输入名称 (例如: yourUsername)"
|
||||
disabled={disabled}
|
||||
/>
|
||||
</Input.Group>
|
||||
);
|
||||
};
|
||||
|
||||
export default SubjectInput;
|
103
frontend-new/src/components/consumer/ClientInfoModal.jsx
Normal file
103
frontend-new/src/components/consumer/ClientInfoModal.jsx
Normal file
@@ -0,0 +1,103 @@
|
||||
/*
|
||||
* Licensed to the Apache Software Foundation (ASF) under one or more
|
||||
* contributor license agreements. See the NOTICE file distributed with
|
||||
* this work for additional information regarding copyright ownership.
|
||||
* The ASF licenses this file to You under the Apache License, Version 2.0
|
||||
* (the "License"); you may not use this file except in compliance with
|
||||
* the License. You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import { Modal, Table, Spin } from 'antd';
|
||||
import { remoteApi } from '../../api/remoteApi/remoteApi';
|
||||
import { useLanguage } from '../../i18n/LanguageContext';
|
||||
|
||||
const ClientInfoModal = ({ visible, group, address, onCancel }) => {
|
||||
const { t } = useLanguage();
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [connectionData, setConnectionData] = useState(null);
|
||||
const [subscriptionData, setSubscriptionData] = useState(null);
|
||||
|
||||
useEffect(() => {
|
||||
const fetchData = async () => {
|
||||
if (!visible) return;
|
||||
|
||||
setLoading(true);
|
||||
try {
|
||||
const connResponse = await remoteApi.queryConsumerConnection(group, address);
|
||||
const topicResponse = await remoteApi.queryTopicByConsumer(group, address);
|
||||
|
||||
if (connResponse.status === 0) setConnectionData(connResponse.data);
|
||||
if (topicResponse.status === 0) setSubscriptionData(topicResponse.data);
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
fetchData();
|
||||
}, [visible, group, address]);
|
||||
|
||||
const connectionColumns = [
|
||||
{ title: 'ClientId', dataIndex: 'clientId' },
|
||||
{ title: 'ClientAddr', dataIndex: 'clientAddr' },
|
||||
{ title: 'Language', dataIndex: 'language' },
|
||||
{ title: 'Version', dataIndex: 'versionDesc' },
|
||||
];
|
||||
|
||||
const subscriptionColumns = [
|
||||
{ title: 'Topic', dataIndex: 'topic' },
|
||||
{ title: 'SubExpression', dataIndex: 'subString' },
|
||||
];
|
||||
|
||||
return (
|
||||
<Modal
|
||||
title={`[${group}]${t.CLIENT}`}
|
||||
visible={visible}
|
||||
onCancel={onCancel}
|
||||
footer={null}
|
||||
width={800}
|
||||
>
|
||||
<Spin spinning={loading}>
|
||||
{connectionData && (
|
||||
<>
|
||||
<Table
|
||||
columns={connectionColumns}
|
||||
dataSource={connectionData.connectionSet}
|
||||
rowKey="clientId"
|
||||
pagination={false}
|
||||
/>
|
||||
<h4>{t.SUBSCRIPTION}</h4>
|
||||
<Table
|
||||
columns={subscriptionColumns}
|
||||
dataSource={
|
||||
subscriptionData?.subscriptionTable
|
||||
? Object.entries(subscriptionData.subscriptionTable).map(([topic, detail]) => ({
|
||||
topic,
|
||||
...detail,
|
||||
}))
|
||||
: []
|
||||
}
|
||||
rowKey="topic"
|
||||
pagination={false}
|
||||
locale={{
|
||||
emptyText: loading ? <Spin size="small" /> : t.NO_DATA
|
||||
}}
|
||||
/>
|
||||
<p>ConsumeType: {connectionData.consumeType}</p>
|
||||
<p>MessageModel: {connectionData.messageModel}</p>
|
||||
</>
|
||||
)}
|
||||
</Spin>
|
||||
</Modal>
|
||||
);
|
||||
};
|
||||
|
||||
export default ClientInfoModal;
|
287
frontend-new/src/components/consumer/ConsumerConfigItem.jsx
Normal file
287
frontend-new/src/components/consumer/ConsumerConfigItem.jsx
Normal file
@@ -0,0 +1,287 @@
|
||||
/*
|
||||
* Licensed to the Apache Software Foundation (ASF) under one or more
|
||||
* contributor license agreements. See the NOTICE file distributed with
|
||||
* this work for additional information regarding copyright ownership.
|
||||
* The ASF licenses this file to You under the Apache License, Version 2.0
|
||||
* (the "License"); you may not use this file except in compliance with
|
||||
* the License. You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import React, { useEffect, useState } from 'react';
|
||||
import { Button, Descriptions, Form, Input, Select, Switch, message } from 'antd';
|
||||
import { remoteApi } from '../../api/remoteApi/remoteApi'; // 确保路径正确
|
||||
|
||||
const { Option } = Select;
|
||||
|
||||
const ConsumerConfigItem = ({ initialConfig, isAddConfig, group, brokerName, allBrokerList, allClusterNames,onCancel, onSuccess, t }) => {
|
||||
const [form] = Form.useForm();
|
||||
const [currentBrokerName, setCurrentBrokerName] = useState(brokerName);
|
||||
|
||||
useEffect(() => {
|
||||
if (initialConfig) {
|
||||
if (!isAddConfig && initialConfig.brokerNameList && initialConfig.brokerNameList.length > 0) {
|
||||
// 更新模式,设置当前BrokerName为第一个(如果只有一个的话,或者您有其他选择逻辑)
|
||||
setCurrentBrokerName(initialConfig.brokerNameList[0]);
|
||||
}
|
||||
|
||||
form.setFieldsValue({
|
||||
...initialConfig.subscriptionGroupConfig,
|
||||
groupName: isAddConfig ? undefined : initialConfig.subscriptionGroupConfig.groupName, // 添加模式下groupName可编辑
|
||||
brokerName: isAddConfig ? [] : initialConfig.brokerNameList, // 更新模式下显示已有的brokerName
|
||||
clusterName: isAddConfig ? [] : initialConfig.clusterNameList, // 更新模式下显示已有的clusterName
|
||||
});
|
||||
} else {
|
||||
// Reset form for add mode or when initialConfig is null (e.g., when the modal is closed)
|
||||
form.resetFields();
|
||||
form.setFieldsValue({
|
||||
groupName: undefined,
|
||||
autoCommit: true,
|
||||
enableAutoCommit: true,
|
||||
enableAutoOffsetReset: true,
|
||||
groupSysFlag: 0,
|
||||
consumeTimeoutMinute: 10,
|
||||
consumeEnable: true,
|
||||
consumeMessageOrderly: false,
|
||||
consumeBroadcastEnable: false,
|
||||
retryQueueNums: 1,
|
||||
retryMaxTimes: 16,
|
||||
brokerId: 0,
|
||||
whichBrokerWhenConsumeSlowly: 0,
|
||||
brokerName: [],
|
||||
clusterName: [],
|
||||
});
|
||||
setCurrentBrokerName(undefined); // 清空当前brokerName
|
||||
}
|
||||
}, [initialConfig, isAddConfig, form]);
|
||||
|
||||
const handleSubmit = async () => {
|
||||
try {
|
||||
const values = await form.validateFields();
|
||||
const numericValues = {
|
||||
retryQueueNums: Number(values.retryQueueNums),
|
||||
retryMaxTimes: Number(values.retryMaxTimes),
|
||||
brokerId: Number(values.brokerId),
|
||||
whichBrokerWhenConsumeSlowly: Number(values.whichBrokerWhenConsumeSlowly),
|
||||
};
|
||||
|
||||
// 确保brokerNameList是数组
|
||||
let finalBrokerNameList = Array.isArray(values.brokerName) ? values.brokerName : [values.brokerName];
|
||||
// 确保clusterNameList是数组
|
||||
let finalClusterNameList = Array.isArray(values.clusterName) ? values.clusterName : [values.clusterName];
|
||||
|
||||
const payload = {
|
||||
subscriptionGroupConfig: {
|
||||
...(initialConfig && initialConfig.subscriptionGroupConfig ? initialConfig.subscriptionGroupConfig : {}), // 保留旧的配置,除非被新值覆盖
|
||||
...values,
|
||||
...numericValues,
|
||||
groupName: isAddConfig ? values.groupName : group, // 添加模式使用表单中的groupName,更新模式使用传入的group
|
||||
},
|
||||
brokerNameList: finalBrokerNameList,
|
||||
clusterNameList: isAddConfig ? finalClusterNameList : null, // 更新模式保留原有clusterNameList
|
||||
};
|
||||
|
||||
const response = await remoteApi.createOrUpdateConsumer(payload);
|
||||
if (response.status === 0) {
|
||||
message.success(t.SUCCESS);
|
||||
onSuccess();
|
||||
} else {
|
||||
message.error(`${t.OPERATION_FAILED}: ${response.errMsg}`);
|
||||
console.error('Failed to create or update consumer:', response.errMsg);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Validation failed or API call error:', error);
|
||||
message.error(t.FORM_VALIDATION_FAILED);
|
||||
} finally {
|
||||
onCancel()
|
||||
}
|
||||
};
|
||||
|
||||
// Helper function to parse input value to number
|
||||
const parseNumber = (event) => {
|
||||
const value = event.target.value;
|
||||
return value === '' ? undefined : Number(value);
|
||||
};
|
||||
|
||||
// 如果是添加模式,并且用户还没有选择brokerName,或者没有clusterName可供选择,则不渲染表单
|
||||
if (isAddConfig && (!allBrokerList || allBrokerList.length === 0 || !allClusterNames || allClusterNames.length === 0)) {
|
||||
return <p>{t.NO_DATA}</p>;
|
||||
}
|
||||
|
||||
return (
|
||||
<div style={{border: '1px solid #e8e8e8', padding: 20, marginBottom: 20, borderRadius: 8}}>
|
||||
{/* 标题根据当前BrokerName或“添加新配置”显示 */}
|
||||
<h3>{isAddConfig ? t.ADD_CONSUMER : `${t.CONFIG_FOR_BROKER}: ${currentBrokerName || 'N/A'}`}</h3>
|
||||
{!isAddConfig && initialConfig && (
|
||||
<Descriptions bordered column={2} style={{marginBottom: 24}} size="small">
|
||||
<Descriptions.Item label={t.CLUSTER_NAME} span={2}>
|
||||
{initialConfig.clusterNameList?.join(', ') || 'N/A'}
|
||||
</Descriptions.Item>
|
||||
<Descriptions.Item label={t.RETRY_POLICY} span={2}>
|
||||
<pre style={{margin: 0, maxHeight: '100px', overflow: 'auto', fontSize: '12px'}}>
|
||||
{JSON.stringify(
|
||||
initialConfig.subscriptionGroupConfig.groupRetryPolicy,
|
||||
null,
|
||||
2
|
||||
) || 'N/A'}
|
||||
</pre>
|
||||
</Descriptions.Item>
|
||||
<Descriptions.Item label={t.CONSUME_TIMEOUT}>
|
||||
{`${initialConfig.subscriptionGroupConfig.consumeTimeoutMinute} ${t.MINUTES}` || 'N/A'}
|
||||
</Descriptions.Item>
|
||||
<Descriptions.Item label={t.SYSTEM_FLAG}>
|
||||
{initialConfig.subscriptionGroupConfig.groupSysFlag || 'N/A'}
|
||||
</Descriptions.Item>
|
||||
</Descriptions>
|
||||
)}
|
||||
|
||||
<Form form={form} layout="vertical">
|
||||
<Form.Item
|
||||
name="groupName"
|
||||
label={t.GROUP_NAME}
|
||||
rules={[{required: true, message: t.CANNOT_BE_EMPTY}]}
|
||||
>
|
||||
<Input disabled={!isAddConfig}/>
|
||||
</Form.Item>
|
||||
|
||||
{isAddConfig && (
|
||||
<Form.Item
|
||||
name="clusterName"
|
||||
label={t.CLUSTER_NAME}
|
||||
rules={[{required: true, message: t.PLEASE_SELECT_CLUSTER_NAME}]}
|
||||
>
|
||||
<Select
|
||||
mode="multiple"
|
||||
placeholder={t.SELECT_CLUSTERS}
|
||||
disabled={!isAddConfig}
|
||||
>
|
||||
{allClusterNames.map((cluster) => (
|
||||
<Option key={cluster} value={cluster}>
|
||||
{cluster}
|
||||
</Option>
|
||||
))}
|
||||
</Select>
|
||||
</Form.Item>
|
||||
)}
|
||||
|
||||
<Form.Item
|
||||
name="brokerName"
|
||||
label={t.BROKER_NAME}
|
||||
rules={[{required: true, message: t.PLEASE_SELECT_BROKER}]}
|
||||
>
|
||||
<Select
|
||||
mode="multiple"
|
||||
placeholder={t.SELECT_BROKERS}
|
||||
disabled={!isAddConfig} // 只有在添加模式下才能选择brokerName
|
||||
onChange={(selectedBrokers) => {
|
||||
if (isAddConfig && selectedBrokers.length > 0) {
|
||||
// 在添加模式下,如果选择了broker,则将第一个选中的broker设置为当前brokerName用于显示
|
||||
setCurrentBrokerName(selectedBrokers[0]);
|
||||
}
|
||||
}}
|
||||
>
|
||||
{allBrokerList.map((broker) => (
|
||||
<Option key={broker} value={broker}>
|
||||
{broker}
|
||||
</Option>
|
||||
))}
|
||||
</Select>
|
||||
</Form.Item>
|
||||
|
||||
|
||||
<Form.Item name="consumeEnable" label={t.CONSUME_ENABLE} valuePropName="checked">
|
||||
<Switch/>
|
||||
</Form.Item>
|
||||
|
||||
<Form.Item name="consumeMessageOrderly" label={t.ORDERLY_CONSUMPTION} valuePropName="checked">
|
||||
<Switch/>
|
||||
</Form.Item>
|
||||
|
||||
<Form.Item name="consumeBroadcastEnable" label={t.BROADCAST_CONSUMPTION} valuePropName="checked">
|
||||
<Switch/>
|
||||
</Form.Item>
|
||||
|
||||
<div style={{display: 'grid', gridTemplateColumns: '1fr 1fr', gap: 16}}>
|
||||
<Form.Item
|
||||
name="retryQueueNums"
|
||||
label={t.RETRY_QUEUES}
|
||||
rules={[{
|
||||
type: 'number',
|
||||
message: t.PLEASE_INPUT_NUMBER,
|
||||
transform: value => Number(value)
|
||||
}, {
|
||||
required: true,
|
||||
message: t.CANNOT_BE_EMPTY
|
||||
}]}
|
||||
getValueFromEvent={parseNumber}
|
||||
>
|
||||
<Input type="number"/>
|
||||
</Form.Item>
|
||||
|
||||
<Form.Item
|
||||
name="retryMaxTimes"
|
||||
label={t.MAX_RETRIES}
|
||||
rules={[{
|
||||
type: 'number',
|
||||
message: t.PLEASE_INPUT_NUMBER,
|
||||
transform: value => Number(value)
|
||||
}, {
|
||||
required: true,
|
||||
message: t.CANNOT_BE_EMPTY
|
||||
}]}
|
||||
getValueFromEvent={parseNumber}
|
||||
>
|
||||
<Input type="number"/>
|
||||
</Form.Item>
|
||||
|
||||
<Form.Item
|
||||
name="brokerId"
|
||||
label={t.BROKER_ID}
|
||||
rules={[{
|
||||
type: 'number',
|
||||
message: t.PLEASE_INPUT_NUMBER,
|
||||
transform: value => Number(value)
|
||||
}, {
|
||||
required: true,
|
||||
message: t.CANNOT_BE_EMPTY
|
||||
}]}
|
||||
getValueFromEvent={parseNumber}
|
||||
>
|
||||
<Input type="number"/>
|
||||
</Form.Item>
|
||||
|
||||
<Form.Item
|
||||
name="whichBrokerWhenConsumeSlowly"
|
||||
label={t.SLOW_CONSUMPTION_BROKER}
|
||||
rules={[{
|
||||
type: 'number',
|
||||
message: t.PLEASE_INPUT_NUMBER,
|
||||
transform: value => Number(value)
|
||||
}, {
|
||||
required: true,
|
||||
message: t.CANNOT_BE_EMPTY
|
||||
}]}
|
||||
getValueFromEvent={parseNumber}
|
||||
>
|
||||
<Input type="number"/>
|
||||
</Form.Item>
|
||||
</div>
|
||||
<div style={{textAlign: 'right', marginTop: 20}}>
|
||||
<Button type="primary" onClick={handleSubmit}>
|
||||
{t.COMMIT}
|
||||
</Button>
|
||||
</div>
|
||||
</Form>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default ConsumerConfigItem;
|
||||
|
169
frontend-new/src/components/consumer/ConsumerConfigModal.jsx
Normal file
169
frontend-new/src/components/consumer/ConsumerConfigModal.jsx
Normal file
@@ -0,0 +1,169 @@
|
||||
/*
|
||||
* Licensed to the Apache Software Foundation (ASF) under one or more
|
||||
* contributor license agreements. See the NOTICE file distributed with
|
||||
* this work for additional information regarding copyright ownership.
|
||||
* The ASF licenses this file to You under the Apache License, Version 2.0
|
||||
* (the "License"); you may not use this file except in compliance with
|
||||
* the License. You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import React, {useEffect, useState} from 'react';
|
||||
import {Button, Modal, Spin} from 'antd';
|
||||
import {remoteApi} from '../../api/remoteApi/remoteApi';
|
||||
import {useLanguage} from '../../i18n/LanguageContext';
|
||||
import ConsumerConfigItem from './ConsumerConfigItem'; // 导入子组件
|
||||
|
||||
const ConsumerConfigModal = ({visible, isAddConfig, group, onCancel, setIsAddConfig, onSuccess}) => {
|
||||
const {t} = useLanguage();
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [allBrokerList, setAllBrokerList] = useState([]); // 存储所有可用的broker
|
||||
const [allClusterNames, setAllClusterNames] = useState([]); // 存储所有可用的cluster names
|
||||
const [initialConfigData, setInitialConfigData] = useState({}); // 存储按brokerName分的初始配置数据
|
||||
|
||||
useEffect(() => {
|
||||
if (visible) {
|
||||
const fetchInitialData = async () => {
|
||||
setLoading(true);
|
||||
try {
|
||||
// Fetch cluster list for broker names and cluster names
|
||||
if(isAddConfig) {
|
||||
const clusterResponse = await remoteApi.getClusterList();
|
||||
if (clusterResponse.status === 0 && clusterResponse.data) {
|
||||
const clusterInfo = clusterResponse.data.clusterInfo;
|
||||
|
||||
const brokers = [];
|
||||
const clusterNames = Object.keys(clusterInfo?.clusterAddrTable || {});
|
||||
|
||||
clusterNames.forEach(clusterName => {
|
||||
const brokersInCluster = clusterInfo?.clusterAddrTable?.[clusterName] || [];
|
||||
brokers.push(...brokersInCluster);
|
||||
});
|
||||
|
||||
setAllBrokerList([...new Set(brokers)]); // 确保brokerName唯一
|
||||
setAllClusterNames(clusterNames);
|
||||
|
||||
} else {
|
||||
console.error('Failed to fetch cluster list:', clusterResponse.errMsg);
|
||||
}
|
||||
}
|
||||
if (!isAddConfig) {
|
||||
// Fetch existing consumer config for update mode
|
||||
const consumerConfigResponse = await remoteApi.queryConsumerConfig(group);
|
||||
if (consumerConfigResponse.status === 0 && consumerConfigResponse.data && consumerConfigResponse.data.length > 0) {
|
||||
const configMap = {};
|
||||
consumerConfigResponse.data.forEach(config => {
|
||||
// 假设每个brokerName有一个独立的配置项
|
||||
config.brokerNameList.forEach(brokerName => {
|
||||
configMap[brokerName] = {
|
||||
...config,
|
||||
// 确保brokerNameList和clusterNameList是数组形式,即使API返回单值
|
||||
brokerNameList: Array.isArray(config.brokerNameList) ? config.brokerNameList : [config.brokerNameList],
|
||||
clusterNameList: Array.isArray(config.clusterNameList) ? config.clusterNameList : [config.clusterNameList]
|
||||
};
|
||||
});
|
||||
});
|
||||
setInitialConfigData(configMap);
|
||||
} else {
|
||||
console.error(`Failed to fetch consumer config for group: ${group}`);
|
||||
onCancel(); // Close modal if config not found
|
||||
}
|
||||
} else {
|
||||
// For add mode, initialize with empty values and allow selecting any broker
|
||||
setInitialConfigData({
|
||||
// 当isAddConfig为true时,我们只提供一个空的配置模板,用户选择broker后会创建新的配置
|
||||
// 在这里,我们将设置一个空的初始配置,供用户选择broker来创建新配置
|
||||
newConfig: {
|
||||
groupName: undefined,
|
||||
subscriptionGroupConfig: {
|
||||
autoCommit: true,
|
||||
enableAutoCommit: true,
|
||||
enableAutoOffsetReset: true,
|
||||
groupSysFlag: 0,
|
||||
consumeTimeoutMinute: 10,
|
||||
consumeEnable: true,
|
||||
consumeMessageOrderly: false,
|
||||
consumeBroadcastEnable: false,
|
||||
retryQueueNums: 1,
|
||||
retryMaxTimes: 16,
|
||||
brokerId: 0,
|
||||
whichBrokerWhenConsumeSlowly: 0,
|
||||
},
|
||||
brokerNameList: [],
|
||||
clusterNameList: []
|
||||
}
|
||||
});
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error in fetching initial data:', error);
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
fetchInitialData();
|
||||
} else {
|
||||
// Reset state when modal is closed
|
||||
setInitialConfigData({});
|
||||
setAllBrokerList([]);
|
||||
setAllClusterNames([]);
|
||||
}
|
||||
}, [visible, isAddConfig, group, onCancel]);
|
||||
|
||||
const getBrokersToRender = () => {
|
||||
if (isAddConfig) {
|
||||
return ['newConfig'];
|
||||
} else {
|
||||
return Object.keys(initialConfigData);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
return (
|
||||
<Modal
|
||||
title={isAddConfig ? t.ADD_CONSUMER : `${t.CONFIG} - ${group}`}
|
||||
visible={visible}
|
||||
onCancel={() => {
|
||||
onCancel();
|
||||
setIsAddConfig(false); // 确保关闭时重置添加模式
|
||||
}}
|
||||
width={800}
|
||||
footer={[
|
||||
<Button key="cancel" onClick={() => {
|
||||
onCancel();
|
||||
setIsAddConfig(false);
|
||||
}}>
|
||||
{t.CLOSE}
|
||||
</Button>,
|
||||
]}
|
||||
style={{top: 20}} // 让弹窗靠上一点,方便内容滚动
|
||||
bodyStyle={{maxHeight: 'calc(100vh - 200px)', overflowY: 'auto'}} // 允许内容滚动
|
||||
>
|
||||
<Spin spinning={loading}>
|
||||
{getBrokersToRender().map(brokerOrKey => (
|
||||
<ConsumerConfigItem
|
||||
key={brokerOrKey} // 使用brokerName作为key
|
||||
initialConfig={initialConfigData[brokerOrKey]}
|
||||
isAddConfig={isAddConfig}
|
||||
group={group} // 传递当前group
|
||||
brokerName={isAddConfig ? undefined : brokerOrKey} // 添加模式下brokerName由用户选择,更新模式下是当前遍历的brokerName
|
||||
allBrokerList={allBrokerList}
|
||||
allClusterNames={allClusterNames}
|
||||
onSuccess={onSuccess}
|
||||
onCancel={onCancel}
|
||||
t={t} // 传递i18n函数
|
||||
/>
|
||||
))}
|
||||
</Spin>
|
||||
</Modal>
|
||||
);
|
||||
};
|
||||
|
||||
export default ConsumerConfigModal;
|
79
frontend-new/src/components/consumer/ConsumerDetailModal.jsx
Normal file
79
frontend-new/src/components/consumer/ConsumerDetailModal.jsx
Normal file
@@ -0,0 +1,79 @@
|
||||
/*
|
||||
* Licensed to the Apache Software Foundation (ASF) under one or more
|
||||
* contributor license agreements. See the NOTICE file distributed with
|
||||
* this work for additional information regarding copyright ownership.
|
||||
* The ASF licenses this file to You under the Apache License, Version 2.0
|
||||
* (the "License"); you may not use this file except in compliance with
|
||||
* the License. You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import { Modal, Table, Spin } from 'antd';
|
||||
import { remoteApi } from '../../api/remoteApi/remoteApi';
|
||||
import { useLanguage } from '../../i18n/LanguageContext';
|
||||
|
||||
const ConsumerDetailModal = ({ visible, group, address, onCancel }) => {
|
||||
const { t } = useLanguage();
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [details, setDetails] = useState([]);
|
||||
|
||||
useEffect(() => {
|
||||
const fetchData = async () => {
|
||||
if (!visible) return;
|
||||
|
||||
setLoading(true);
|
||||
try {
|
||||
const response = await remoteApi.queryTopicByConsumer(group, address);
|
||||
if (response.status === 0) {
|
||||
setDetails(response.data);
|
||||
}
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
fetchData();
|
||||
}, [visible, group, address]);
|
||||
|
||||
const queueColumns = [
|
||||
{ title: 'Broker', dataIndex: 'brokerName' },
|
||||
{ title: 'Queue', dataIndex: 'queueId' },
|
||||
{ title: 'BrokerOffset', dataIndex: 'brokerOffset' },
|
||||
{ title: 'ConsumerOffset', dataIndex: 'consumerOffset' },
|
||||
{ title: 'DiffTotal', dataIndex: 'diffTotal' },
|
||||
{ title: 'LastTimestamp', dataIndex: 'lastTimestamp' },
|
||||
];
|
||||
|
||||
return (
|
||||
<Modal
|
||||
title={`[${group}]${t.CONSUME_DETAIL}`}
|
||||
visible={visible}
|
||||
onCancel={onCancel}
|
||||
footer={null}
|
||||
width={1200}
|
||||
>
|
||||
<Spin spinning={loading}>
|
||||
{details.map((consumeDetail, index) => (
|
||||
<div key={index}>
|
||||
<Table
|
||||
columns={queueColumns}
|
||||
dataSource={consumeDetail.queueStatInfoList}
|
||||
rowKey="queueId"
|
||||
pagination={false}
|
||||
/>
|
||||
</div>
|
||||
))}
|
||||
</Spin>
|
||||
</Modal>
|
||||
);
|
||||
};
|
||||
|
||||
export default ConsumerDetailModal;
|
110
frontend-new/src/components/consumer/DeleteConsumerModal.jsx
Normal file
110
frontend-new/src/components/consumer/DeleteConsumerModal.jsx
Normal file
@@ -0,0 +1,110 @@
|
||||
/*
|
||||
* Licensed to the Apache Software Foundation (ASF) under one or more
|
||||
* contributor license agreements. See the NOTICE file distributed with
|
||||
* this work for additional information regarding copyright ownership.
|
||||
* The ASF licenses this file to You under the Apache License, Version 2.0
|
||||
* (the "License"); you may not use this file except in compliance with
|
||||
* the License. You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import { Modal, Spin, Checkbox, Button, notification } from 'antd';
|
||||
import { remoteApi } from '../../api/remoteApi/remoteApi';
|
||||
import { useLanguage } from '../../i18n/LanguageContext';
|
||||
|
||||
const DeleteConsumerModal = ({ visible, group, onCancel, onSuccess }) => {
|
||||
const { t } = useLanguage();
|
||||
const [brokerList, setBrokerList] = useState([]);
|
||||
const [selectedBrokers, setSelectedBrokers] = useState([]);
|
||||
const [loading, setLoading] = useState(false);
|
||||
|
||||
// 获取Broker列表
|
||||
useEffect(() => {
|
||||
const fetchBrokers = async () => {
|
||||
if (!visible) return;
|
||||
|
||||
setLoading(true);
|
||||
try {
|
||||
const response = await remoteApi.fetchBrokerNameList(group);
|
||||
if (response.status === 0) {
|
||||
setBrokerList(response.data);
|
||||
}
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
fetchBrokers();
|
||||
}, [visible, group]);
|
||||
|
||||
// 处理删除提交
|
||||
const handleDelete = async () => {
|
||||
if (selectedBrokers.length === 0) {
|
||||
notification.warning({ message: t.PLEASE_SELECT_BROKER });
|
||||
return;
|
||||
}
|
||||
|
||||
setLoading(true);
|
||||
try {
|
||||
const response = await remoteApi.deleteConsumerGroup(
|
||||
group,
|
||||
selectedBrokers
|
||||
);
|
||||
|
||||
if (response.status === 0) {
|
||||
notification.success({ message: t.DELETE_SUCCESS });
|
||||
onSuccess();
|
||||
onCancel();
|
||||
}
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<Modal
|
||||
title={`${t.DELETE_CONSUMER_GROUP} - ${group}`}
|
||||
visible={visible}
|
||||
onCancel={onCancel}
|
||||
footer={[
|
||||
<Button key="cancel" onClick={onCancel}>
|
||||
{t.CANCEL}
|
||||
</Button>,
|
||||
<Button
|
||||
key="delete"
|
||||
type="primary"
|
||||
danger
|
||||
loading={loading}
|
||||
onClick={handleDelete}
|
||||
>
|
||||
{t.CONFIRM_DELETE}
|
||||
</Button>
|
||||
]}
|
||||
>
|
||||
<Spin spinning={loading}>
|
||||
<div style={{ marginBottom: 16 }}>{t.SELECT_DELETE_BROKERS}:</div>
|
||||
<Checkbox.Group
|
||||
style={{ width: '100%' }}
|
||||
value={selectedBrokers}
|
||||
onChange={values => setSelectedBrokers(values)}
|
||||
>
|
||||
{brokerList.map(broker => (
|
||||
<div key={broker}>
|
||||
<Checkbox value={broker}>{broker}</Checkbox>
|
||||
</div>
|
||||
))}
|
||||
</Checkbox.Group>
|
||||
</Spin>
|
||||
</Modal>
|
||||
);
|
||||
};
|
||||
|
||||
export default DeleteConsumerModal;
|
@@ -0,0 +1,76 @@
|
||||
/*
|
||||
* Licensed to the Apache Software Foundation (ASF) under one or more
|
||||
* contributor license agreements. See the NOTICE file distributed with
|
||||
* this work for additional information regarding copyright ownership.
|
||||
* The ASF licenses this file to You under the Apache License, Version 2.0
|
||||
* (the "License"); you may not use this file except in compliance with
|
||||
* the License. You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import { Button, DatePicker, Form, Modal, Select } from "antd";
|
||||
import React, { useEffect, useState } from "react";
|
||||
|
||||
const ConsumerResetOffsetDialog = ({ visible, onClose, topic, allConsumerGroupList, handleResetOffset, t }) => {
|
||||
const [form] = Form.useForm();
|
||||
const [selectedConsumerGroup, setSelectedConsumerGroup] = useState([]);
|
||||
const [selectedTime, setSelectedTime] = useState(null);
|
||||
|
||||
useEffect(() => {
|
||||
if (!visible) {
|
||||
setSelectedConsumerGroup([]);
|
||||
setSelectedTime(null);
|
||||
form.resetFields();
|
||||
}
|
||||
}, [visible, form]);
|
||||
|
||||
const handleResetButtonClick = () => {
|
||||
handleResetOffset(selectedConsumerGroup, selectedTime ? selectedTime.valueOf() : null);
|
||||
};
|
||||
|
||||
return (
|
||||
<Modal
|
||||
title={`${topic} ${t.RESET_OFFSET}`}
|
||||
open={visible}
|
||||
onCancel={onClose}
|
||||
footer={[
|
||||
<Button key="reset" type="primary" onClick={handleResetButtonClick}>
|
||||
{t.RESET}
|
||||
</Button>,
|
||||
<Button key="close" onClick={onClose}>
|
||||
{t.CLOSE}
|
||||
</Button>,
|
||||
]}
|
||||
>
|
||||
<Form form={form} layout="horizontal" labelCol={{ span: 6 }} wrapperCol={{ span: 18 }}>
|
||||
<Form.Item label={t.SUBSCRIPTION_GROUP} required>
|
||||
<Select
|
||||
mode="multiple"
|
||||
placeholder={t.SELECT_CONSUMER_GROUP}
|
||||
value={selectedConsumerGroup}
|
||||
onChange={setSelectedConsumerGroup}
|
||||
options={allConsumerGroupList.map(group => ({ value: group, label: group }))}
|
||||
/>
|
||||
</Form.Item>
|
||||
<Form.Item label={t.TIME} required>
|
||||
<DatePicker
|
||||
showTime
|
||||
format="YYYY-MM-DD HH:mm:ss"
|
||||
value={selectedTime}
|
||||
onChange={setSelectedTime}
|
||||
style={{ width: '100%' }}
|
||||
/>
|
||||
</Form.Item>
|
||||
</Form>
|
||||
</Modal>
|
||||
);
|
||||
};
|
||||
|
||||
export default ConsumerResetOffsetDialog;
|
96
frontend-new/src/components/topic/ConsumerViewDialog.jsx
Normal file
96
frontend-new/src/components/topic/ConsumerViewDialog.jsx
Normal file
@@ -0,0 +1,96 @@
|
||||
/*
|
||||
* Licensed to the Apache Software Foundation (ASF) under one or more
|
||||
* contributor license agreements. See the NOTICE file distributed with
|
||||
* this work for additional information regarding copyright ownership.
|
||||
* The ASF licenses this file to You under the Apache License, Version 2.0
|
||||
* (the "License"); you may not use this file except in compliance with
|
||||
* the License. You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import moment from "moment/moment";
|
||||
import {Button, Modal, Table} from "antd";
|
||||
import React from "react";
|
||||
|
||||
const ConsumerViewDialog = ({ visible, onClose, topic, consumerData, consumerGroupCount, t }) => {
|
||||
const columns = [
|
||||
{ title: t.BROKER, dataIndex: 'brokerName', key: 'brokerName', align: 'center' },
|
||||
{ title: t.QUEUE, dataIndex: 'queueId', key: 'queueId', align: 'center' },
|
||||
{ title: t.CONSUMER_CLIENT, dataIndex: 'clientInfo', key: 'clientInfo', align: 'center' },
|
||||
{ title: t.BROKER_OFFSET, dataIndex: 'brokerOffset', key: 'brokerOffset', align: 'center' },
|
||||
{ title: t.CONSUMER_OFFSET, dataIndex: 'consumerOffset', key: 'consumerOffset', align: 'center' },
|
||||
{
|
||||
title: t.DIFF_TOTAL,
|
||||
dataIndex: 'diffTotal',
|
||||
key: 'diffTotal',
|
||||
align: 'center',
|
||||
render: (_, record) => record.brokerOffset - record.consumerOffset,
|
||||
},
|
||||
{
|
||||
title: t.LAST_TIME_STAMP,
|
||||
dataIndex: 'lastTimestamp',
|
||||
key: 'lastTimestamp',
|
||||
align: 'center',
|
||||
render: (text) => moment(text).format('YYYY-MM-DD HH:mm:ss'),
|
||||
},
|
||||
];
|
||||
|
||||
return (
|
||||
<Modal
|
||||
title={`${topic} ${t.SUBSCRIPTION_GROUP}`}
|
||||
open={visible}
|
||||
onCancel={onClose}
|
||||
width={1000}
|
||||
footer={[
|
||||
<Button key="close" onClick={onClose}>
|
||||
{t.CLOSE}
|
||||
</Button>,
|
||||
]}
|
||||
>
|
||||
{consumerGroupCount === 0 ? (
|
||||
<div>{t.NO_DATA} {t.SUBSCRIPTION_GROUP}</div>
|
||||
) : (
|
||||
consumerData && Object.entries(consumerData).map(([consumerGroup, consumeDetail]) => (
|
||||
<div key={consumerGroup} style={{ marginBottom: '24px' }}>
|
||||
<Table
|
||||
bordered
|
||||
pagination={false}
|
||||
showHeader={false}
|
||||
dataSource={[{ consumerGroup, diffTotal: consumeDetail.diffTotal, lastTimestamp: consumeDetail.lastTimestamp }]}
|
||||
columns={[
|
||||
{ title: t.SUBSCRIPTION_GROUP, dataIndex: 'consumerGroup', key: 'consumerGroup' },
|
||||
{ title: t.DELAY, dataIndex: 'diffTotal', key: 'diffTotal' },
|
||||
{
|
||||
title: t.LAST_CONSUME_TIME,
|
||||
dataIndex: 'lastTimestamp',
|
||||
key: 'lastTimestamp',
|
||||
render: (text) => moment(text).format('YYYY-MM-DD HH:mm:ss'),
|
||||
},
|
||||
]}
|
||||
rowKey="consumerGroup"
|
||||
size="small"
|
||||
style={{ marginBottom: '12px' }}
|
||||
/>
|
||||
<Table
|
||||
bordered
|
||||
pagination={false}
|
||||
dataSource={consumeDetail.queueStatInfoList}
|
||||
columns={columns}
|
||||
rowKey={(record, index) => `${record.brokerName}-${record.queueId}-${index}`}
|
||||
size="small"
|
||||
/>
|
||||
</div>
|
||||
))
|
||||
)}
|
||||
</Modal>
|
||||
);
|
||||
};
|
||||
|
||||
export default ConsumerViewDialog;
|
@@ -0,0 +1,65 @@
|
||||
/*
|
||||
* Licensed to the Apache Software Foundation (ASF) under one or more
|
||||
* contributor license agreements. See the NOTICE file distributed with
|
||||
* this work for additional information regarding copyright ownership.
|
||||
* The ASF licenses this file to You under the Apache License, Version 2.0
|
||||
* (the "License"); you may not use this file except in compliance with
|
||||
* the License. You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import {Button, Modal, Table} from "antd";
|
||||
import React from "react";
|
||||
|
||||
const ResetOffsetResultDialog = ({ visible, onClose, result, t }) => {
|
||||
return (
|
||||
<Modal
|
||||
title="ResetResult"
|
||||
open={visible}
|
||||
onCancel={onClose}
|
||||
footer={[
|
||||
<Button key="close" onClick={onClose}>
|
||||
{t.CLOSE}
|
||||
</Button>,
|
||||
]}
|
||||
>
|
||||
{result && Object.entries(result).map(([groupName, groupData]) => (
|
||||
<div key={groupName} style={{ marginBottom: '16px', border: '1px solid #f0f0f0', padding: '10px' }}>
|
||||
<Table
|
||||
dataSource={[{ groupName, status: groupData.status }]}
|
||||
columns={[
|
||||
{ title: 'GroupName', dataIndex: 'groupName', key: 'groupName' },
|
||||
{ title: 'State', dataIndex: 'status', key: 'status' },
|
||||
]}
|
||||
pagination={false}
|
||||
rowKey="groupName"
|
||||
size="small"
|
||||
bordered
|
||||
/>
|
||||
{groupData.rollbackStatsList === null ? (
|
||||
<div>You Should Check It Yourself</div>
|
||||
) : (
|
||||
<Table
|
||||
dataSource={groupData.rollbackStatsList.map((item, index) => ({ key: index, item }))}
|
||||
columns={[{ dataIndex: 'item', key: 'item' }]}
|
||||
pagination={false}
|
||||
rowKey="key"
|
||||
size="small"
|
||||
bordered
|
||||
showHeader={false}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
))}
|
||||
</Modal>
|
||||
);
|
||||
};
|
||||
|
||||
export default ResetOffsetResultDialog;
|
111
frontend-new/src/components/topic/RouterViewDialog.jsx
Normal file
111
frontend-new/src/components/topic/RouterViewDialog.jsx
Normal file
@@ -0,0 +1,111 @@
|
||||
/*
|
||||
* Licensed to the Apache Software Foundation (ASF) under one or more
|
||||
* contributor license agreements. See the NOTICE file distributed with
|
||||
* this work for additional information regarding copyright ownership.
|
||||
* The ASF licenses this file to You under the Apache License, Version 2.0
|
||||
* (the "License"); you may not use this file except in compliance with
|
||||
* the License. You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import { Button, Modal, Table } from "antd";
|
||||
import React from "react";
|
||||
|
||||
const RouterViewDialog = ({ visible, onClose, topic, routeData, t }) => {
|
||||
const brokerColumns = [
|
||||
{
|
||||
title: 'Broker',
|
||||
dataIndex: 'brokerName',
|
||||
key: 'brokerName',
|
||||
},
|
||||
{
|
||||
title: 'Broker Addrs',
|
||||
key: 'brokerAddrs',
|
||||
render: (_, record) => (
|
||||
<Table
|
||||
dataSource={Object.entries(record.brokerAddrs || []).map(([key, value]) => ({ key, idx: key, address: value }))}
|
||||
columns={[
|
||||
{ title: 'Index', dataIndex: 'idx', key: 'idx' },
|
||||
{ title: 'Address', dataIndex: 'address', key: 'address' },
|
||||
]}
|
||||
pagination={false}
|
||||
bordered
|
||||
size="small"
|
||||
/>
|
||||
),
|
||||
},
|
||||
];
|
||||
|
||||
const queueColumns = [
|
||||
{
|
||||
title: t.BROKER_NAME,
|
||||
dataIndex: 'brokerName',
|
||||
key: 'brokerName',
|
||||
},
|
||||
{
|
||||
title: t.READ_QUEUE_NUMS,
|
||||
dataIndex: 'readQueueNums',
|
||||
key: 'readQueueNums',
|
||||
},
|
||||
{
|
||||
title: t.WRITE_QUEUE_NUMS,
|
||||
dataIndex: 'writeQueueNums',
|
||||
key: 'writeQueueNums',
|
||||
},
|
||||
{
|
||||
title: t.PERM,
|
||||
dataIndex: 'perm',
|
||||
key: 'perm',
|
||||
},
|
||||
];
|
||||
|
||||
return (
|
||||
<Modal
|
||||
title={`${topic}${t.ROUTER}`}
|
||||
open={visible}
|
||||
onCancel={onClose}
|
||||
width={800}
|
||||
footer={[
|
||||
<Button key="close" onClick={onClose}>
|
||||
{t.CLOSE}
|
||||
</Button>,
|
||||
]}
|
||||
>
|
||||
<div className="limit_height">
|
||||
<div>
|
||||
<h3>Broker Datas:</h3>
|
||||
{routeData?.brokerDatas?.map((item, index) => (
|
||||
<div key={index} style={{ marginBottom: '15px', border: '1px solid #d9d9d9', padding: '10px' }}>
|
||||
<Table
|
||||
dataSource={[item]}
|
||||
columns={brokerColumns}
|
||||
pagination={false}
|
||||
bordered
|
||||
size="small"
|
||||
/>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
<div style={{ marginTop: '20px' }}>
|
||||
<h3>{t.QUEUE_DATAS}:</h3>
|
||||
<Table
|
||||
dataSource={routeData?.queueDatas || []}
|
||||
columns={queueColumns}
|
||||
pagination={false}
|
||||
bordered
|
||||
size="small"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</Modal>
|
||||
);
|
||||
};
|
||||
|
||||
export default RouterViewDialog;
|
65
frontend-new/src/components/topic/SendResultDialog.jsx
Normal file
65
frontend-new/src/components/topic/SendResultDialog.jsx
Normal file
@@ -0,0 +1,65 @@
|
||||
/*
|
||||
* Licensed to the Apache Software Foundation (ASF) under one or more
|
||||
* contributor license agreements. See the NOTICE file distributed with
|
||||
* this work for additional information regarding copyright ownership.
|
||||
* The ASF licenses this file to You under the Apache License, Version 2.0
|
||||
* (the "License"); you may not use this file except in compliance with
|
||||
* the License. You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import {Button, Form, Modal, Table} from "antd";
|
||||
import React from "react";
|
||||
|
||||
const SendResultDialog = ({ visible, onClose, result, t }) => {
|
||||
return (
|
||||
<Modal
|
||||
title="SendResult"
|
||||
open={visible}
|
||||
onCancel={onClose}
|
||||
footer={[
|
||||
<Button key="close" onClick={onClose}>
|
||||
{t.CLOSE}
|
||||
</Button>,
|
||||
]}
|
||||
>
|
||||
<Form layout="horizontal">
|
||||
<Table
|
||||
bordered
|
||||
dataSource={
|
||||
result
|
||||
? Object.entries(result).map(([key, value], index) => ({
|
||||
key: index,
|
||||
label: key,
|
||||
value: typeof value === 'object' ? JSON.stringify(value, null, 2) : String(value),
|
||||
}))
|
||||
: []
|
||||
}
|
||||
columns={[
|
||||
{ dataIndex: 'label', key: 'label' },
|
||||
{
|
||||
dataIndex: 'value',
|
||||
key: 'value',
|
||||
render: (text) => <pre style={{ whiteSpace: 'pre-wrap', margin: 0 }}>{text}</pre>,
|
||||
},
|
||||
]}
|
||||
pagination={false}
|
||||
showHeader={false}
|
||||
rowKey="key"
|
||||
size="small"
|
||||
/>
|
||||
</Form>
|
||||
</Modal>
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
|
||||
export default SendResultDialog;
|
103
frontend-new/src/components/topic/SendTopicMessageDialog.jsx
Normal file
103
frontend-new/src/components/topic/SendTopicMessageDialog.jsx
Normal file
@@ -0,0 +1,103 @@
|
||||
/*
|
||||
* Licensed to the Apache Software Foundation (ASF) under one or more
|
||||
* contributor license agreements. See the NOTICE file distributed with
|
||||
* this work for additional information regarding copyright ownership.
|
||||
* The ASF licenses this file to You under the Apache License, Version 2.0
|
||||
* (the "License"); you may not use this file except in compliance with
|
||||
* the License. You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import {Button, Checkbox, Form, Input, Modal} from "antd";
|
||||
import React, {useEffect} from "react";
|
||||
import {remoteApi} from "../../api/remoteApi/remoteApi";
|
||||
|
||||
const SendTopicMessageDialog = ({
|
||||
visible,
|
||||
onClose,
|
||||
topic,
|
||||
setSendResultData,
|
||||
setIsSendResultModalVisible,
|
||||
setIsSendTopicMessageModalVisible,
|
||||
message,
|
||||
t,
|
||||
}) => {
|
||||
const [form] = Form.useForm();
|
||||
|
||||
useEffect(() => {
|
||||
if (visible) {
|
||||
form.setFieldsValue({
|
||||
topic: topic,
|
||||
tag: '',
|
||||
key: '',
|
||||
messageBody: '',
|
||||
traceEnabled: false,
|
||||
});
|
||||
} else {
|
||||
form.resetFields();
|
||||
}
|
||||
}, [visible, topic, form]);
|
||||
|
||||
const handleSendTopicMessage = async () => {
|
||||
try {
|
||||
const values = await form.validateFields();
|
||||
const result = await remoteApi.sendTopicMessage(values);
|
||||
if (result.status === 0) {
|
||||
setSendResultData(result.data);
|
||||
setIsSendResultModalVisible(true);
|
||||
setIsSendTopicMessageModalVisible(false);
|
||||
} else {
|
||||
message.error(result.errMsg);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("Error sending message:", error);
|
||||
message.error("Failed to send message");
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<Modal
|
||||
title={`${t.SEND}[${topic}]${t.MESSAGE}`}
|
||||
open={visible}
|
||||
onCancel={onClose}
|
||||
footer={[
|
||||
<Button key="commit" type="primary" onClick={handleSendTopicMessage}>
|
||||
{t.COMMIT}
|
||||
</Button>,
|
||||
<Button key="close" onClick={onClose}>
|
||||
{t.CLOSE}
|
||||
</Button>,
|
||||
]}
|
||||
>
|
||||
<Form form={form} layout="horizontal" labelCol={{ span: 6 }} wrapperCol={{ span: 18 }}>
|
||||
<Form.Item label={t.TOPIC} name="topic">
|
||||
<Input disabled />
|
||||
</Form.Item>
|
||||
<Form.Item label={t.TAG} name="tag">
|
||||
<Input />
|
||||
</Form.Item>
|
||||
<Form.Item label={t.KEY} name="key">
|
||||
<Input />
|
||||
</Form.Item>
|
||||
<Form.Item label={t.MESSAGE_BODY} name="messageBody" rules={[{ required: true, message: t.REQUIRED }]}>
|
||||
<Input.TextArea
|
||||
style={{ maxHeight: '200px', minHeight: '200px', resize: 'none' }}
|
||||
rows={8}
|
||||
/>
|
||||
</Form.Item>
|
||||
<Form.Item label={t.ENABLE_MESSAGE_TRACE} name="traceEnabled" valuePropName="checked">
|
||||
<Checkbox />
|
||||
</Form.Item>
|
||||
</Form>
|
||||
</Modal>
|
||||
);
|
||||
};
|
||||
|
||||
export default SendTopicMessageDialog;
|
@@ -0,0 +1,70 @@
|
||||
/*
|
||||
* Licensed to the Apache Software Foundation (ASF) under one or more
|
||||
* contributor license agreements. See the NOTICE file distributed with
|
||||
* this work for additional information regarding copyright ownership.
|
||||
* The ASF licenses this file to You under the Apache License, Version 2.0
|
||||
* (the "License"); you may not use this file except in compliance with
|
||||
* the License. You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import { Button, Form, message, Modal, Select } from "antd";
|
||||
import React, { useEffect, useState } from "react";
|
||||
|
||||
const SkipMessageAccumulateDialog = ({ visible, onClose, topic, allConsumerGroupList, handleSkipMessageAccumulate, t }) => {
|
||||
const [form] = Form.useForm();
|
||||
const [selectedConsumerGroup, setSelectedConsumerGroup] = useState([]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!visible) {
|
||||
setSelectedConsumerGroup([]);
|
||||
form.resetFields();
|
||||
}
|
||||
}, [visible, form]);
|
||||
|
||||
const handleCommit = () => {
|
||||
if (!selectedConsumerGroup.length) {
|
||||
message.error(t.PLEASE_SELECT_GROUP);
|
||||
return;
|
||||
}
|
||||
handleSkipMessageAccumulate(selectedConsumerGroup);
|
||||
onClose();
|
||||
};
|
||||
|
||||
return (
|
||||
<Modal
|
||||
title={`${topic} ${t.SKIP_MESSAGE_ACCUMULATE}`}
|
||||
open={visible}
|
||||
onCancel={onClose}
|
||||
footer={[
|
||||
<Button key="commit" type="primary" onClick={handleCommit}>
|
||||
{t.COMMIT}
|
||||
</Button>,
|
||||
<Button key="close" onClick={onClose}>
|
||||
{t.CLOSE}
|
||||
</Button>,
|
||||
]}
|
||||
>
|
||||
<Form form={form} layout="horizontal" labelCol={{ span: 6 }} wrapperCol={{ span: 18 }}>
|
||||
<Form.Item label={t.SUBSCRIPTION_GROUP} required>
|
||||
<Select
|
||||
mode="multiple"
|
||||
placeholder={t.SELECT_CONSUMER_GROUP}
|
||||
value={selectedConsumerGroup}
|
||||
onChange={setSelectedConsumerGroup}
|
||||
options={allConsumerGroupList.map(group => ({ value: group, label: group }))}
|
||||
/>
|
||||
</Form.Item>
|
||||
</Form>
|
||||
</Modal>
|
||||
);
|
||||
};
|
||||
|
||||
export default SkipMessageAccumulateDialog;
|
66
frontend-new/src/components/topic/StatsViewDialog.jsx
Normal file
66
frontend-new/src/components/topic/StatsViewDialog.jsx
Normal file
@@ -0,0 +1,66 @@
|
||||
/*
|
||||
* Licensed to the Apache Software Foundation (ASF) under one or more
|
||||
* contributor license agreements. See the NOTICE file distributed with
|
||||
* this work for additional information regarding copyright ownership.
|
||||
* The ASF licenses this file to You under the Apache License, Version 2.0
|
||||
* (the "License"); you may not use this file except in compliance with
|
||||
* the License. You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import moment from "moment/moment";
|
||||
import {Button, Modal, Table} from "antd";
|
||||
import React from "react";
|
||||
|
||||
const StatsViewDialog = ({ visible, onClose, topic, statsData, t }) => {
|
||||
const columns = [
|
||||
{ title: t.QUEUE, dataIndex: 'queue', key: 'queue', align: 'center' },
|
||||
{ title: t.MIN_OFFSET, dataIndex: 'minOffset', key: 'minOffset', align: 'center' },
|
||||
{ title: t.MAX_OFFSET, dataIndex: 'maxOffset', key: 'maxOffset', align: 'center' },
|
||||
{
|
||||
title: t.LAST_UPDATE_TIME_STAMP,
|
||||
dataIndex: 'lastUpdateTimestamp',
|
||||
key: 'lastUpdateTimestamp',
|
||||
align: 'center',
|
||||
render: (text) => moment(text).format('YYYY-MM-DD HH:mm:ss'),
|
||||
},
|
||||
];
|
||||
|
||||
const dataSource = statsData?.offsetTable ? Object.entries(statsData.offsetTable).map(([queue, info]) => ({
|
||||
key: queue,
|
||||
queue: queue,
|
||||
...info,
|
||||
})) : [];
|
||||
|
||||
return (
|
||||
<Modal
|
||||
title={`[${topic}]${t.STATUS}`}
|
||||
open={visible}
|
||||
onCancel={onClose}
|
||||
width={800}
|
||||
footer={[
|
||||
<Button key="close" onClick={onClose}>
|
||||
{t.CLOSE}
|
||||
</Button>,
|
||||
]}
|
||||
>
|
||||
<Table
|
||||
bordered
|
||||
dataSource={dataSource}
|
||||
columns={columns}
|
||||
pagination={false}
|
||||
rowKey="key"
|
||||
size="small"
|
||||
/>
|
||||
</Modal>
|
||||
);
|
||||
};
|
||||
|
||||
export default StatsViewDialog;
|
65
frontend-new/src/components/topic/TopicModifyDialog.jsx
Normal file
65
frontend-new/src/components/topic/TopicModifyDialog.jsx
Normal file
@@ -0,0 +1,65 @@
|
||||
/*
|
||||
* 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.
|
||||
*/
|
||||
|
||||
// TopicModifyDialog.js
|
||||
import { Button, Modal } from "antd";
|
||||
import React from "react";
|
||||
import TopicSingleModifyForm from './TopicSingleModifyForm';
|
||||
|
||||
const TopicModifyDialog = ({
|
||||
visible,
|
||||
onClose,
|
||||
initialData,
|
||||
bIsUpdate,
|
||||
writeOperationEnabled,
|
||||
allClusterNameList,
|
||||
allBrokerNameList,
|
||||
onSubmit,
|
||||
t,
|
||||
}) => {
|
||||
|
||||
return (
|
||||
<Modal
|
||||
title={bIsUpdate ? t.TOPIC_CHANGE : t.TOPIC_ADD}
|
||||
open={visible}
|
||||
onCancel={onClose}
|
||||
width={700}
|
||||
footer={[
|
||||
<Button key="close" onClick={onClose}>
|
||||
{t.CLOSE}
|
||||
</Button>,
|
||||
]}
|
||||
Style={{ maxHeight: '70vh', overflowY: 'auto' }}
|
||||
>
|
||||
{initialData.map((data, index) => (
|
||||
<TopicSingleModifyForm
|
||||
key={index}
|
||||
initialData={data}
|
||||
bIsUpdate={bIsUpdate}
|
||||
writeOperationEnabled={writeOperationEnabled}
|
||||
allClusterNameList={allClusterNameList}
|
||||
allBrokerNameList={allBrokerNameList}
|
||||
onSubmit={onSubmit}
|
||||
formIndex={index}
|
||||
t={t}
|
||||
/>
|
||||
))}
|
||||
</Modal>
|
||||
);
|
||||
};
|
||||
|
||||
export default TopicModifyDialog;
|
144
frontend-new/src/components/topic/TopicSingleModifyForm.jsx
Normal file
144
frontend-new/src/components/topic/TopicSingleModifyForm.jsx
Normal file
@@ -0,0 +1,144 @@
|
||||
/*
|
||||
* 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.
|
||||
*/
|
||||
|
||||
// TopicSingleModifyForm.js
|
||||
import React, { useEffect } from "react";
|
||||
import {Button, Form, Input, Select, Divider, Row, Col} from "antd";
|
||||
|
||||
const TopicSingleModifyForm = ({
|
||||
initialData,
|
||||
bIsUpdate,
|
||||
writeOperationEnabled,
|
||||
allClusterNameList,
|
||||
allBrokerNameList,
|
||||
onSubmit,
|
||||
formIndex,
|
||||
t,
|
||||
}) => {
|
||||
const [form] = Form.useForm();
|
||||
|
||||
useEffect(() => {
|
||||
if (initialData) {
|
||||
form.setFieldsValue(initialData);
|
||||
} else {
|
||||
form.resetFields();
|
||||
}
|
||||
}, [initialData, form, formIndex]);
|
||||
|
||||
const handleFormSubmit = () => {
|
||||
form.validateFields()
|
||||
.then(values => {
|
||||
const updatedValues = { ...values };
|
||||
// 提交时,如果 clusterNameList 或 brokerNameList 为空,则填充所有可用的名称
|
||||
if(!bIsUpdate){
|
||||
if (!updatedValues.clusterNameList || updatedValues.clusterNameList.length === 0) {
|
||||
updatedValues.clusterNameList = allClusterNameList;
|
||||
}
|
||||
if (!updatedValues.brokerNameList || updatedValues.brokerNameList.length === 0) {
|
||||
updatedValues.brokerNameList = allBrokerNameList;
|
||||
}
|
||||
}
|
||||
onSubmit(updatedValues, formIndex); // 传递 formIndex
|
||||
})
|
||||
.catch(info => {
|
||||
console.log('Validate Failed:', info);
|
||||
});
|
||||
};
|
||||
|
||||
const messageTypeOptions = [
|
||||
{ value: 'TRANSACTION', label: 'TRANSACTION' },
|
||||
{ value: 'FIFO', label: 'FIFO' },
|
||||
{ value: 'DELAY', label: 'DELAY' },
|
||||
{ value: 'NORMAL', label: 'NORMAL' },
|
||||
];
|
||||
|
||||
return (
|
||||
<div style={{ paddingBottom: 24 }}>
|
||||
{bIsUpdate && <Divider orientation="left">{`${t.TOPIC_CONFIG} - ${initialData.brokerNameList ? initialData.brokerNameList.join(', ') : t.UNKNOWN_BROKER}`}</Divider>}
|
||||
<Row justify="center"> {/* 使用 Row 居中内容 */}
|
||||
<Col span={16}> {/* 表单内容占据 12 栅格宽度,并自动居中 */}
|
||||
<Form
|
||||
form={form}
|
||||
layout="horizontal"
|
||||
labelCol={{ span: 8 }}
|
||||
wrapperCol={{ span: 16 }}
|
||||
>
|
||||
<Form.Item label={t.CLUSTER_NAME} name="clusterNameList">
|
||||
<Select
|
||||
mode="multiple"
|
||||
disabled={bIsUpdate}
|
||||
placeholder={t.SELECT_CLUSTER_NAME}
|
||||
options={allClusterNameList.map(name => ({ value: name, label: name }))}
|
||||
/>
|
||||
</Form.Item>
|
||||
<Form.Item label="BROKER_NAME" name="brokerNameList">
|
||||
<Select
|
||||
mode="multiple"
|
||||
disabled={bIsUpdate}
|
||||
placeholder={t.SELECT_BROKER_NAME}
|
||||
options={allBrokerNameList.map(name => ({ value: name, label: name }))}
|
||||
/>
|
||||
</Form.Item>
|
||||
<Form.Item
|
||||
label={t.TOPIC_NAME}
|
||||
name="topicName"
|
||||
rules={[{ required: true, message: `${t.TOPIC_NAME}${t.CANNOT_BE_EMPTY}` }]}
|
||||
>
|
||||
<Input disabled={bIsUpdate} />
|
||||
</Form.Item>
|
||||
<Form.Item label={t.MESSAGE_TYPE} name="messageType">
|
||||
<Select
|
||||
disabled={bIsUpdate}
|
||||
options={messageTypeOptions}
|
||||
/>
|
||||
</Form.Item>
|
||||
<Form.Item
|
||||
label={t.WRITE_QUEUE_NUMS}
|
||||
name="writeQueueNums"
|
||||
rules={[{ required: true, message: `${t.WRITE_QUEUE_NUMS}${t.CANNOT_BE_EMPTY}` }]}
|
||||
>
|
||||
<Input disabled={!writeOperationEnabled} />
|
||||
</Form.Item>
|
||||
<Form.Item
|
||||
label={t.READ_QUEUE_NUMS}
|
||||
name="readQueueNums"
|
||||
rules={[{ required: true, message: `${t.READ_QUEUE_NUMS}${t.CANNOT_BE_EMPTY}` }]}
|
||||
>
|
||||
<Input disabled={!writeOperationEnabled} />
|
||||
</Form.Item>
|
||||
<Form.Item
|
||||
label={t.PERM}
|
||||
name="perm"
|
||||
rules={[{ required: true, message: `${t.PERM}${t.CANNOT_BE_EMPTY}` }]}
|
||||
>
|
||||
<Input disabled={!writeOperationEnabled} />
|
||||
</Form.Item>
|
||||
{!initialData.sysFlag && writeOperationEnabled && (
|
||||
<Form.Item wrapperCol={{ offset: 8, span: 16 }}>
|
||||
<Button type="primary" onClick={handleFormSubmit}>
|
||||
{t.COMMIT}
|
||||
</Button>
|
||||
</Form.Item>
|
||||
)}
|
||||
</Form>
|
||||
</Col>
|
||||
</Row>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default TopicSingleModifyForm;
|
37
frontend-new/src/i18n/LanguageContext.js
Normal file
37
frontend-new/src/i18n/LanguageContext.js
Normal file
@@ -0,0 +1,37 @@
|
||||
/*
|
||||
* Licensed to the Apache Software Foundation (ASF) under one or more
|
||||
* contributor license agreements. See the NOTICE file distributed with
|
||||
* this work for additional information regarding copyright ownership.
|
||||
* The ASF licenses this file to You under the Apache License, Version 2.0
|
||||
* (the "License"); you may not use this file except in compliance with
|
||||
* the License. You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import React, { createContext, useState, useContext } from 'react';
|
||||
import { translations } from '../i18n';
|
||||
|
||||
const LanguageContext = createContext({
|
||||
lang: 'en',
|
||||
setLang: () => {},
|
||||
t: translations['en'], // 当前语言的文本资源
|
||||
});
|
||||
|
||||
export const LanguageProvider = ({ children }) => {
|
||||
const [lang, setLang] = useState('en');
|
||||
const t = translations[lang] || translations['en'];
|
||||
return (
|
||||
<LanguageContext.Provider value={{ lang, setLang, t }}>
|
||||
{children}
|
||||
</LanguageContext.Provider>
|
||||
);
|
||||
};
|
||||
|
||||
export const useLanguage = () => useContext(LanguageContext);
|
542
frontend-new/src/i18n/index.js
Normal file
542
frontend-new/src/i18n/index.js
Normal file
@@ -0,0 +1,542 @@
|
||||
/*
|
||||
* 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.
|
||||
*/
|
||||
|
||||
export const translations = {
|
||||
zh: {
|
||||
"SELECT_TRACE_TOPIC_PLACEHOLDER": "请选择消息轨迹主题",
|
||||
"TRACE_TOPIC_HINT": "消息轨迹主题",
|
||||
"ONLY_RETURN_64_MESSAGES": "仅返回64条消息",
|
||||
"SELECT_TOPIC_PLACEHOLDER": "请选择主题",
|
||||
"MESSAGE_ID_TOPIC_HINT": "消息ID主题",
|
||||
"OPERATION_FAILED": "操作失败",
|
||||
"FORM_VALIDATION_FAILED": "表单验证失败",
|
||||
"CONFIG_FOR_BROKER": "配置代理",
|
||||
"RETRY_POLICY": "重试策略",
|
||||
"CONSUME_TIMEOUT": "消费超时",
|
||||
"MINUTES": "分钟",
|
||||
"SYSTEM_FLAG": "系统标志",
|
||||
"GROUP_NAME": "组名称",
|
||||
"PLEASE_SELECT_CLUSTER_NAME": "请选择集群名称",
|
||||
"SELECT_CLUSTERS": "选择集群",
|
||||
"SELECT_BROKERS": "选择代理",
|
||||
"CONSUME_ENABLE": "启用消费",
|
||||
"ORDERLY_CONSUMPTION": "有序消费",
|
||||
"BROADCAST_CONSUMPTION": "广播消费",
|
||||
"RETRY_QUEUES": "重试队列",
|
||||
"MAX_RETRIES": "最大重试次数",
|
||||
"BROKER_ID": "代理ID",
|
||||
"SLOW_CONSUMPTION_BROKER": "慢消费代理",
|
||||
"PLEASE_INPUT_NUMBER": "请输入数字",
|
||||
"TOPIC_CONFIG": "主题配置",
|
||||
"TOPIC_ADD": "添加主题",
|
||||
"SELECT_CLUSTER_NAME": "请选择集群",
|
||||
"FETCH_TOPIC_FAILED": "获取主题列表失败",
|
||||
"CONFIRM_DELETE": "确认删除",
|
||||
"CANCEL": "取消",
|
||||
"SELECT_DELETE_BROKERS":"请选择在哪个Broker删除消费者组",
|
||||
"DELETE_CONSUMER_GROUP":"删除消费者组",
|
||||
"ENGLISH": "英文",
|
||||
"ADD_CONSUMER":"添加消费者",
|
||||
"CHINESE": "简体中文",
|
||||
"CANNOT_BE_EMPTY": "不能为空",
|
||||
"TITLE": "RocketMQ仪表板",
|
||||
"CLOSE": "关闭",
|
||||
"NO": "编号",
|
||||
"ADDRESS": "地址",
|
||||
"VERSION": "版本",
|
||||
"PRO_MSG_TPS": "生产消息TPS",
|
||||
"CUS_MSG_TPS": "消费消息TPS",
|
||||
"YESTERDAY_PRO_COUNT": "昨日生产总数",
|
||||
"YESTERDAY_CUS_COUNT": "昨日消费总数",
|
||||
"TODAY_PRO_COUNT": "今天生产总数",
|
||||
"TODAY_CUS_COUNT": "今天消费总数",
|
||||
"INSTANCE": "实例",
|
||||
"SPLIT": "分片",
|
||||
"CLUSTER": "集群",
|
||||
"CLUSTER_DETAIL": "集群详情",
|
||||
"COMMIT": "提交",
|
||||
"TOPIC": "主题",
|
||||
"SUBSCRIPTION_GROUP":"订阅组",
|
||||
"PRODUCER_GROUP":"生产组",
|
||||
"CONSUMER":"消费者",
|
||||
"PRODUCER":"生产者",
|
||||
"MESSAGE":"消息",
|
||||
"MESSAGE_DETAIL":"消息详情",
|
||||
"RESEND_MESSAGE":"重新发送",
|
||||
"VIEW_EXCEPTION":"查看异常",
|
||||
"DLQ_MESSAGE":"死信消息",
|
||||
"MESSAGETRACE":"消息轨迹",
|
||||
"OPERATION": "操作",
|
||||
"ADD": "新增",
|
||||
"UPDATE": "更新",
|
||||
"STATUS": "状态",
|
||||
"ROUTER": "路由",
|
||||
"MANAGE": "管理",
|
||||
"CONFIG": "配置",
|
||||
"SEND_MSG": "发送消息",
|
||||
"RESET_CUS_OFFSET": "重置消费位点",
|
||||
"SKIP_MESSAGE_ACCUMULATE":"跳过堆积",
|
||||
"DELETE": "删除",
|
||||
"CHANGE_LANG": "更换语言",
|
||||
"CHANGE_VERSION": "更换版本",
|
||||
"BROKER": "Broker",
|
||||
"NORMAL": "普通",
|
||||
"RETRY": "重试",
|
||||
"FIFO": "顺序",
|
||||
"TRANSACTION": "事务",
|
||||
"UNSPECIFIED": "未指定",
|
||||
"DLQ": "死信",
|
||||
"QUANTITY":"数量",
|
||||
"TYPE":"类型",
|
||||
"MODE":"模式",
|
||||
"DELAY":"延迟",
|
||||
"DASHBOARD":"驾驶舱",
|
||||
"CONSUME_DETAIL":"消费详情",
|
||||
"CLIENT":"终端",
|
||||
"LAST_CONSUME_TIME":"最后消费时间",
|
||||
"TIME":"时间点",
|
||||
"RESET":"重置",
|
||||
"DATE":"日期",
|
||||
"NO_DATA":"暂无数据",
|
||||
"SEARCH":"搜索",
|
||||
"BEGIN":"开始",
|
||||
"END":"结束",
|
||||
"TOPIC_CHANGE":"修改主题",
|
||||
"SEND":"发送",
|
||||
"SUBSCRIPTION_CHANGE":"修改订阅",
|
||||
"QUEUE":"队列",
|
||||
"MIN_OFFSET":"最小位点",
|
||||
"MAX_OFFSET":"最大位点",
|
||||
"LAST_UPDATE_TIME_STAMP":"上次更新时间",
|
||||
"QUEUE_DATAS":"队列信息",
|
||||
"READ_QUEUE_NUMS":"读队列数量",
|
||||
"WRITE_QUEUE_NUMS":"写队列数量",
|
||||
"PERM":"perm",
|
||||
"TAG":"标签",
|
||||
"KEY":"值",
|
||||
"MESSAGE_BODY":"消息主体",
|
||||
"TOPIC_NAME":"主题名",
|
||||
"ORDER":"顺序",
|
||||
"CONSUMER_CLIENT":"消费者终端",
|
||||
"BROKER_OFFSET":"代理者位点",
|
||||
"CONSUMER_OFFSET":"消费者位点",
|
||||
"DIFF_TOTAL":"差值",
|
||||
"LAST_TIME_STAMP":"上次时间",
|
||||
"RESET_OFFSET":"重置位点",
|
||||
"CLUSTER_NAME":"集群名",
|
||||
"OPS":"运维",
|
||||
"PROXY":"代理",
|
||||
"AUTO_REFRESH":"自动刷新",
|
||||
"REFRESH":"刷新",
|
||||
"LOGOUT":"退出",
|
||||
"LOGIN":"登录",
|
||||
"USER_NAME":"用户名",
|
||||
"PASSWORD":"密码",
|
||||
"SYSTEM":"系统",
|
||||
"WELCOME":"您好,欢迎使用RocketMQ仪表盘",
|
||||
"ENABLE_MESSAGE_TRACE":"开启消息轨迹",
|
||||
"MESSAGE_TRACE_DETAIL":"消息轨迹详情",
|
||||
"TRACE_TOPIC":"消息轨迹主题",
|
||||
"SELECT_TRACE_TOPIC":"选择消息轨迹主题",
|
||||
"EXPORT": "导出",
|
||||
"NO_MATCH_RESULT": "没有查到符合条件的结果",
|
||||
"BATCH_RESEND": "批量重发",
|
||||
"BATCH_EXPORT": "批量导出",
|
||||
"WHITE_LIST":"白名单",
|
||||
"ACCOUNT_INFO":"账户信息",
|
||||
"IS_ADMIN":"是否管理员",
|
||||
"DEFAULT_TOPIC_PERM":"topic默认权限",
|
||||
"DEFAULT_GROUP_PERM":"消费组默认权限",
|
||||
"TOPIC_PERM":"topic权限",
|
||||
"GROUP_PERM":"消费组权限",
|
||||
"SYNCHRONIZE":"同步",
|
||||
"SHOW":"显示",
|
||||
"HIDE":"隐藏",
|
||||
"MESSAGE_TYPE":"消息类型",
|
||||
"MESSAGE_TYPE_UNSPECIFIED": "未指定,为普通消息",
|
||||
"MESSAGE_TYPE_NORMAL": "普通消息",
|
||||
"MESSAGE_TYPE_FIFO": "顺序消息",
|
||||
"MESSAGE_TYPE_DELAY": "定时/延时消息",
|
||||
"MESSAGE_TYPE_TRANSACTION": "事务消息",
|
||||
"UPDATE_TIME": "更新时间",
|
||||
"TREND": "趋势",
|
||||
"PROXY_CONFIG": "代理配置",
|
||||
"READ_MORE": "阅读更多",
|
||||
"FETCH_PROXY_LIST_FAILED": "获取代理列表失败",
|
||||
"INPUT_PROXY_ADDR_REQUIRED": "请输入代理地址",
|
||||
"SUCCESS": "成功",
|
||||
"ADD_PROXY_FAILED": "添加代理失败",
|
||||
"INPUT_PROXY_ADDR": "输入代理地址",
|
||||
"NO_CONFIG_DATA": "无配置数据",
|
||||
"FETCH_MESSAGE_DETAIL_FAILED": "获取消息详情失败",
|
||||
"MESSAGE_INFO": "消息信息",
|
||||
"MESSAGE_PROPERTIES": "消息属性",
|
||||
"SHOW_ALL_CONTENT": "显示全部内容",
|
||||
"MESSAGE_TRACKING": "消息追踪",
|
||||
"CONSUMER_GROUP": "消费者组",
|
||||
"PLEASE_SELECT_BROKER": "请选择Broker",
|
||||
"DELETE_SUCCESS": "删除成功",
|
||||
"FAILED_TO_FETCH_DATA": "获取数据失败",
|
||||
"REFRESH_SUCCESS": "刷新成功",
|
||||
"REFRESH_FAILED": "刷新失败",
|
||||
"REFRESHED": "已刷新",
|
||||
"QUERY_BROKER_HISTORY_FAILED": "查询Broker历史失败",
|
||||
"QUERY_TOPIC_HISTORY_FAILED": "查询Topic历史失败",
|
||||
"QUERY_CLUSTER_LIST_FAILED": "查询集群列表失败",
|
||||
"QUERY_TOPIC_CURRENT_FAILED": "查询当前Topic失败",
|
||||
"BROKER_NAME": "Broker名称",
|
||||
"BROKER_ADDR": "Broker地址",
|
||||
"WARNING": "警告",
|
||||
"PLEASE_SELECT_CONSUMER_GROUP": "请选择消费者组",
|
||||
"END_TIME_LATER_THAN_BEGIN_TIME": "结束时间应晚于开始时间",
|
||||
"NO_RESULT": "无结果",
|
||||
"QUERY_FAILED": "查询失败",
|
||||
"MESSAGE_ID_AND_CONSUMER_GROUP_REQUIRED": "消息ID和消费者组为必填项",
|
||||
"RESEND_SUCCESS": "重新发送成功",
|
||||
"RESULT": "结果",
|
||||
"TOPIC_AND_KEY_REQUIRED": "Topic和Key为必填项",
|
||||
"MESSAGE_ID_REQUIRED": "消息ID为必填项",
|
||||
"REFRESHING_TOPIC_LIST": "正在刷新Topic列表",
|
||||
"TOPIC_OPERATION_SUCCESS": "Topic操作成功",
|
||||
"ARE_YOU_SURE_TO_DELETE": "您确定要删除吗?",
|
||||
"YES": "是",
|
||||
"NOT": "否",
|
||||
"BLUE": "蓝色",
|
||||
"GREEN": "绿色",
|
||||
"PINK": "粉色",
|
||||
"DEFAULT": "默认",
|
||||
"INVALID_IP_ADDRESSES": "以下IP地址不合法: ",
|
||||
"ENABLED": "启用",
|
||||
"DISABLED": "禁用",
|
||||
"GET_USERS_FAILED": "获取用户列表失败: ",
|
||||
"UNKNOWN_ERROR": "未知错误",
|
||||
"GET_USERS_EXCEPTION": "获取用户列表异常",
|
||||
"N_A": "N/A",
|
||||
"INVALID_OR_EMPTY_ACL_DATA": "收到无效或空的ACL数据。",
|
||||
"GET_ACLS_FAILED": "获取ACL列表失败: ",
|
||||
"GET_ACLS_EXCEPTION": "获取ACL列表异常",
|
||||
"USER_DELETE_SUCCESS": "用户删除成功",
|
||||
"USER_DELETE_FAILED": "用户删除失败: ",
|
||||
"USER_DELETE_EXCEPTION": "用户删除异常",
|
||||
"USER_UPDATE_SUCCESS": "用户更新成功",
|
||||
"USER_CREATE_SUCCESS": "用户创建成功",
|
||||
"SAVE_USER_FAILED": "保存用户失败",
|
||||
"ACL_DELETE_SUCCESS": "ACL 删除成功",
|
||||
"ACL_DELETE_FAILED": "ACL 删除失败: ",
|
||||
"ACL_DELETE_EXCEPTION": "ACL 删除异常",
|
||||
"ACL_UPDATE_SUCCESS": "ACL 更新成功",
|
||||
"ACL_UPDATE_FAILED": "ACL 更新失败: ",
|
||||
"ACL_CREATE_SUCCESS": "ACL 创建成功",
|
||||
"ACL_CREATE_FAILED": "ACL 创建失败: ",
|
||||
"SAVE_ACL_FAILED": "保存 ACL 失败",
|
||||
"USERNAME": "用户名",
|
||||
"VIEW": "查看",
|
||||
"USER_TYPE": "用户类型",
|
||||
"USER_STATUS": "用户状态",
|
||||
"MODIFY": "修改",
|
||||
"CONFIRM_DELETE_USER": "确定删除此用户吗?",
|
||||
"USERNAME_SUBJECT": "用户名/Subject",
|
||||
"POLICY_TYPE": "策略类型",
|
||||
"RESOURCE_NAME": "资源名",
|
||||
"OPERATION_TYPE": "操作类型",
|
||||
"SOURCE_IP": "来源IP",
|
||||
"DECISION": "决策",
|
||||
"CONFIRM_DELETE_ACL": "确定删除此ACL吗?",
|
||||
"ACL_MANAGEMENT": "ACL 管理",
|
||||
"ACL_USERS": "ACL 用户",
|
||||
"ACL_PERMISSIONS": "ACL 权限",
|
||||
"ADD_USER": "添加用户",
|
||||
"ADD_ACL_PERMISSION": "添加 ACL 权限",
|
||||
"SEARCH_PLACEHOLDER": "搜索...",
|
||||
"USER": "用户",
|
||||
"ACL_PERMISSION": "ACL 权限",
|
||||
"EDIT_USER": "编辑用户",
|
||||
"CONFIRM": "确认",
|
||||
"PLEASE_ENTER_USERNAME": "请输入用户名!",
|
||||
"PLEASE_ENTER_PASSWORD": "请输入密码!",
|
||||
"PLEASE_SELECT_USER_TYPE": "请选择用户类型!",
|
||||
"PLEASE_SELECT_USER_STATUS": "请选择用户状态!",
|
||||
"EDIT_ACL_PERMISSION": "编辑 ACL 权限",
|
||||
"SUBJECT_LABEL": "Subject (例如: User:yourUsername)",
|
||||
"PLEASE_ENTER_SUBJECT": "请输入 Subject!",
|
||||
"PLEASE_ENTER_POLICY_TYPE": "请输入策略类型!",
|
||||
"RESOURCE": "资源",
|
||||
"PLEASE_ADD_RESOURCE": "请添加资源!",
|
||||
"ENTER_IP_HINT": "请输入 IP 地址,按回车键添加,支持 IPv4、IPv6 和 CIDR",
|
||||
"PLEASE_ENTER_DECISION": "请输入决策!",
|
||||
"MENU": "菜单",
|
||||
},
|
||||
en: {
|
||||
"DEFAULT": "Default",
|
||||
"BLUE": "Blue",
|
||||
"GREEN": "Green",
|
||||
"PINK": "Pink",
|
||||
"NOT": "No",
|
||||
"ARE_YOU_SURE_TO_DELETE": "Are you sure to delete?",
|
||||
"YES": "Yes",
|
||||
"SELECT_TRACE_TOPIC_PLACEHOLDER": "Please select trace topic",
|
||||
"TRACE_TOPIC_HINT": "Trace Topic",
|
||||
"ONLY_RETURN_64_MESSAGES": "Only return 64 messages",
|
||||
"SELECT_TOPIC_PLACEHOLDER": "Please select topic",
|
||||
"MESSAGE_ID_TOPIC_HINT": "Message ID Topic",
|
||||
"TOPIC_ADD": "Add Topic",
|
||||
"SKIP_MESSAGE_ACCUMULATE":"Skip Message Accumulate",
|
||||
"OPERATION_FAILED": "Operation Failed",
|
||||
"FORM_VALIDATION_FAILED": "Form Validation Failed",
|
||||
"ADD_CONSUMER": "Add Consumer",
|
||||
"CONFIG_FOR_BROKER": "Config for Broker",
|
||||
"RETRY_POLICY": "Retry Policy",
|
||||
"CONSUME_TIMEOUT": "Consume Timeout",
|
||||
"MINUTES": "Minutes",
|
||||
"SYSTEM_FLAG": "System Flag",
|
||||
"GROUP_NAME": "Group Name",
|
||||
"CANNOT_BE_EMPTY": "Cannot be empty",
|
||||
"PLEASE_SELECT_CLUSTER_NAME": "Please select cluster name",
|
||||
"SELECT_CLUSTERS": "Select Clusters",
|
||||
"SELECT_BROKERS": "Select Brokers",
|
||||
"CONSUME_ENABLE": "Consume Enable",
|
||||
"ORDERLY_CONSUMPTION": "Orderly Consumption",
|
||||
"BROADCAST_CONSUMPTION": "Broadcast Consumption",
|
||||
"RETRY_QUEUES": "Retry Queues",
|
||||
"MAX_RETRIES": "Max Retries",
|
||||
"BROKER_ID": "Broker ID",
|
||||
"SLOW_CONSUMPTION_BROKER": "Slow Consumption Broker",
|
||||
"PLEASE_INPUT_NUMBER": "Please input number",
|
||||
"FETCH_TOPIC_FAILED": "Failed to fetch topic list",
|
||||
"ENGLISH": "English",
|
||||
"CHINESE": "Chinese",
|
||||
"TITLE": "RocketMQ-Dashboard",
|
||||
"CLOSE": "Close",
|
||||
"NO": "NO.",
|
||||
"ADDRESS": "Address",
|
||||
"VERSION": "Version",
|
||||
"PRO_MSG_TPS": "Produce Message TPS",
|
||||
"CUS_MSG_TPS": "Consumer Message TPS",
|
||||
"YESTERDAY_PRO_COUNT": "Yesterday Produce Count",
|
||||
"YESTERDAY_CUS_COUNT": "Yesterday Consume Count",
|
||||
"TODAY_PRO_COUNT": "Today Produce Count",
|
||||
"TODAY_CUS_COUNT": "Today Consume Count",
|
||||
"INSTANCE": "Instance",
|
||||
"SPLIT": "Broker",
|
||||
"CLUSTER": "Cluster",
|
||||
"CLUSTER_DETAIL": "Cluster Detail",
|
||||
"TOPIC": "Topic",
|
||||
"SUBSCRIPTION_GROUP":"SubscriptionGroup",
|
||||
"PRODUCER_GROUP":"ProducerGroup",
|
||||
"CONSUMER":"Consumer",
|
||||
"PRODUCER":"Producer",
|
||||
"MESSAGE":"Message",
|
||||
"MESSAGE_DETAIL":"Message Detail",
|
||||
"RESEND_MESSAGE":"Resend Message",
|
||||
"VIEW_EXCEPTION":"View Exception",
|
||||
"MESSAGETRACE":"MessageTrace",
|
||||
"DLQ_MESSAGE":"DLQMessage",
|
||||
"COMMIT": "Commit",
|
||||
"OPERATION": "Operation",
|
||||
"ADD": "Add",
|
||||
"UPDATE": "Update",
|
||||
"STATUS": "Status",
|
||||
"ROUTER": "Router",
|
||||
"MANAGE": "Manage",
|
||||
"CONFIG": "Config",
|
||||
"SEND_MSG": "Send Massage",
|
||||
"RESET_CUS_OFFSET": "Reset Consumer Offset",
|
||||
"DELETE": "Delete",
|
||||
"CHANGE_LANG": "ChangeLanguage",
|
||||
"CHANGE_VERSION": "ChangeVersion",
|
||||
"BROKER": "Broker",
|
||||
"NORMAL": "NORMAL",
|
||||
"RETRY": "RETRY",
|
||||
"FIFO": "FIFO",
|
||||
"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":"LastConsumeTime",
|
||||
"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":"minOffset",
|
||||
"MAX_OFFSET":"maxOffset",
|
||||
"LAST_UPDATE_TIME_STAMP":"lastUpdateTimeStamp",
|
||||
"QUEUE_DATAS":"queueDatas",
|
||||
"READ_QUEUE_NUMS":"readQueueNums",
|
||||
"WRITE_QUEUE_NUMS":"writeQueueNums",
|
||||
"PERM":"perm",
|
||||
"TAG":"Tag",
|
||||
"KEY":"Key",
|
||||
"MESSAGE_BODY":"Message Body",
|
||||
"TOPIC_NAME":"topicName",
|
||||
"ORDER":"order",
|
||||
"CONSUMER_CLIENT":"consumerClient",
|
||||
"BROKER_OFFSET":"brokerOffset",
|
||||
"CONSUMER_OFFSET":"consumerOffset",
|
||||
"DIFF_TOTAL":"diffTotal",
|
||||
"LAST_TIME_STAMP":"lastTimeStamp",
|
||||
"RESET_OFFSET":"resetOffset",
|
||||
"CLUSTER_NAME":"clusterName",
|
||||
"OPS":"OPS",
|
||||
"PROXY":"Proxy",
|
||||
"AUTO_REFRESH":"AUTO_REFRESH",
|
||||
"REFRESH":"REFRESH",
|
||||
"LOGOUT":"Logout",
|
||||
"LOGIN":"Login",
|
||||
"USER_NAME":"Username",
|
||||
"PASSWORD":"Password",
|
||||
"SYSTEM":"SYSTEM",
|
||||
"WELCOME":"Hi, welcome using RocketMQ Dashboard",
|
||||
"ENABLE_MESSAGE_TRACE":"Enable Message Trace",
|
||||
"MESSAGE_TRACE_DETAIL":"Message Trace Detail",
|
||||
"TRACE_TOPIC":"TraceTopic",
|
||||
"SELECT_TRACE_TOPIC":"selectTraceTopic",
|
||||
"EXPORT": "export",
|
||||
"NO_MATCH_RESULT": "no match result",
|
||||
"BATCH_RESEND": "batchReSend",
|
||||
"BATCH_EXPORT": "batchExport",
|
||||
"WHITE_LIST":"White List",
|
||||
"ACCOUNT_INFO":"Account Info",
|
||||
"IS_ADMIN":"Is Admin",
|
||||
"DEFAULT_TOPIC_PERM":"Default Topic Permission",
|
||||
"DEFAULT_GROUP_PERM":"Default Group Permission",
|
||||
"TOPIC_PERM":"Topic Permission",
|
||||
"GROUP_PERM":"Group Permission",
|
||||
"SYNCHRONIZE":"Synchronize Data",
|
||||
"SHOW":"Show",
|
||||
"HIDE":"Hide",
|
||||
"MESSAGE_TYPE":"messageType",
|
||||
"MESSAGE_TYPE_UNSPECIFIED": "UNSPECIFIED, is NORMAL",
|
||||
"MESSAGE_TYPE_NORMAL": "NORMAL",
|
||||
"MESSAGE_TYPE_FIFO": "FIFO",
|
||||
"MESSAGE_TYPE_DELAY": "DELAY",
|
||||
"MESSAGE_TYPE_TRANSACTION": "TRANSACTION",
|
||||
"UPDATE_TIME": "Update Time",
|
||||
"TREND": "trend",
|
||||
"FETCH_PROXY_LIST_FAILED": "Failed to fetch proxy list",
|
||||
"INPUT_PROXY_ADDR_REQUIRED": "Input proxy address required",
|
||||
"SUCCESS": "Success",
|
||||
"ADD_PROXY_FAILED": "Failed to add proxy",
|
||||
"INPUT_PROXY_ADDR": "Input proxy address",
|
||||
"NO_CONFIG_DATA": "No configuration data",
|
||||
"FETCH_MESSAGE_DETAIL_FAILED": "Failed to fetch message details",
|
||||
"MESSAGE_INFO": "Message info",
|
||||
"MESSAGE_PROPERTIES": "Message properties",
|
||||
"SHOW_ALL_CONTENT": "Show all content",
|
||||
"MESSAGE_TRACKING": "Message tracking",
|
||||
"CONSUMER_GROUP": "Consumer group",
|
||||
"PLEASE_SELECT_BROKER": "Please select a broker",
|
||||
"DELETE_SUCCESS": "Delete successful",
|
||||
"FAILED_TO_FETCH_DATA": "Failed to fetch data",
|
||||
"REFRESH_SUCCESS": "Refresh successful",
|
||||
"REFRESH_FAILED": "Refresh failed",
|
||||
"REFRESHED": "Refreshed",
|
||||
"QUERY_BROKER_HISTORY_FAILED": "Failed to query broker history",
|
||||
"QUERY_TOPIC_HISTORY_FAILED": "Failed to query topic history",
|
||||
"QUERY_CLUSTER_LIST_FAILED": "Failed to query cluster list",
|
||||
"QUERY_TOPIC_CURRENT_FAILED": "Failed to query current topic",
|
||||
"BROKER_NAME": "Broker name",
|
||||
"BROKER_ADDR": "Broker address",
|
||||
"WARNING": "Warning",
|
||||
"PLEASE_SELECT_CONSUMER_GROUP": "Please select a consumer group",
|
||||
"END_TIME_LATER_THAN_BEGIN_TIME": "End time should be later than begin time",
|
||||
"NO_RESULT": "No result",
|
||||
"QUERY_FAILED": "Query failed",
|
||||
"MESSAGE_ID_AND_CONSUMER_GROUP_REQUIRED": "Message ID and consumer group required",
|
||||
"RESEND_SUCCESS": "Resend successful",
|
||||
"RESULT": "Result",
|
||||
"TOPIC_AND_KEY_REQUIRED": "Topic and key required",
|
||||
"MESSAGE_ID_REQUIRED": "Message ID required",
|
||||
"REFRESHING_TOPIC_LIST": "Refreshing topic list",
|
||||
"TOPIC_OPERATION_SUCCESS": "Topic operation successful",
|
||||
"INVALID_IP_ADDRESSES": "The following IP addresses are invalid: ",
|
||||
"ENABLED": "Enabled",
|
||||
"DISABLED": "Disabled",
|
||||
"GET_USERS_FAILED": "Failed to get user list: ",
|
||||
"UNKNOWN_ERROR": "Unknown error",
|
||||
"GET_USERS_EXCEPTION": "Exception getting user list",
|
||||
"N_A": "N/A",
|
||||
"INVALID_OR_EMPTY_ACL_DATA": "Received invalid or empty ACL data.",
|
||||
"GET_ACLS_FAILED": "Failed to get ACL list: ",
|
||||
"GET_ACLS_EXCEPTION": "Exception getting ACL list",
|
||||
"USER_DELETE_SUCCESS": "User deleted successfully",
|
||||
"USER_DELETE_FAILED": "User deletion failed: ",
|
||||
"USER_DELETE_EXCEPTION": "User deletion exception",
|
||||
"USER_UPDATE_SUCCESS": "User updated successfully",
|
||||
"USER_CREATE_SUCCESS": "User created successfully",
|
||||
"SAVE_USER_FAILED": "Failed to save user",
|
||||
"ACL_DELETE_SUCCESS": "ACL deleted successfully",
|
||||
"ACL_DELETE_FAILED": "ACL deletion failed: ",
|
||||
"ACL_DELETE_EXCEPTION": "ACL deletion exception",
|
||||
"ACL_UPDATE_SUCCESS": "ACL updated successfully",
|
||||
"ACL_UPDATE_FAILED": "ACL update failed: ",
|
||||
"ACL_CREATE_SUCCESS": "ACL created successfully",
|
||||
"ACL_CREATE_FAILED": "ACL creation failed: ",
|
||||
"SAVE_ACL_FAILED": "Failed to save ACL",
|
||||
"USERNAME": "Username",
|
||||
"VIEW": "View",
|
||||
"USER_TYPE": "User Type",
|
||||
"USER_STATUS": "User Status",
|
||||
"MODIFY": "Modify",
|
||||
"CONFIRM_DELETE_USER": "Are you sure you want to delete this user?",
|
||||
"USERNAME_SUBJECT": "Username/Subject",
|
||||
"POLICY_TYPE": "Policy Type",
|
||||
"RESOURCE_NAME": "Resource Name",
|
||||
"OPERATION_TYPE": "Operation Type",
|
||||
"SOURCE_IP": "Source IP",
|
||||
"DECISION": "Decision",
|
||||
"CONFIRM_DELETE_ACL": "Are you sure you want to delete this ACL?",
|
||||
"ACL_MANAGEMENT": "ACL Management",
|
||||
"ACL_USERS": "ACL Users",
|
||||
"ACL_PERMISSIONS": "ACL Permissions",
|
||||
"ADD_USER": "Add User",
|
||||
"ADD_ACL_PERMISSION": "Add ACL Permission",
|
||||
"SEARCH_PLACEHOLDER": "Search ...",
|
||||
"USER": "User",
|
||||
"ACL_PERMISSION": "ACL Permission",
|
||||
"EDIT_USER": "Edit User",
|
||||
"CANCEL": "Cancel",
|
||||
"CONFIRM": "Confirm",
|
||||
"PLEASE_ENTER_USERNAME": "Please enter username!",
|
||||
"PLEASE_ENTER_PASSWORD": "Please enter password!",
|
||||
"PLEASE_SELECT_USER_TYPE": "Please select user type!",
|
||||
"PLEASE_SELECT_USER_STATUS": "Please select user status!",
|
||||
"EDIT_ACL_PERMISSION": "Edit ACL Permission",
|
||||
"SUBJECT_LABEL": "Subject (e.g.: User:yourUsername)",
|
||||
"PLEASE_ENTER_SUBJECT": "Please enter Subject!",
|
||||
"PLEASE_ENTER_POLICY_TYPE": "Please enter policy type!",
|
||||
"RESOURCE": "Resource",
|
||||
"PLEASE_ADD_RESOURCE": "Please add resource!",
|
||||
"ENTER_IP_HINT": "Please enter IP address, press Enter to add. Supports IPv4, IPv6, and CIDR.",
|
||||
"PLEASE_ENTER_DECISION": "Please enter decision!",
|
||||
"MENU": "Menu",
|
||||
|
||||
}
|
||||
|
||||
};
|
@@ -14,6 +14,7 @@
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
body {
|
||||
margin: 0;
|
||||
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Oxygen',
|
@@ -14,17 +14,30 @@
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import React from 'react';
|
||||
import ReactDOM from 'react-dom';
|
||||
import ReactDOM from 'react-dom/client';
|
||||
import './index.css';
|
||||
import App from './App';
|
||||
import { App as AntdApp } from 'antd';
|
||||
import reportWebVitals from './reportWebVitals';
|
||||
import {LanguageProvider} from "./i18n/LanguageContext";
|
||||
import {Provider} from "react-redux";
|
||||
import store from './store';
|
||||
|
||||
const root = ReactDOM.createRoot(document.getElementById('root'));
|
||||
root.render(
|
||||
|
||||
<LanguageProvider>
|
||||
<React.StrictMode>
|
||||
<AntdApp>
|
||||
<Provider store={store}>
|
||||
<App/>
|
||||
</Provider>
|
||||
</AntdApp>
|
||||
</React.StrictMode>
|
||||
</LanguageProvider>
|
||||
|
||||
ReactDOM.render(
|
||||
<React.StrictMode>
|
||||
<App />
|
||||
</React.StrictMode>,
|
||||
document.getElementById('root')
|
||||
);
|
||||
|
||||
// If you want to start measuring performance in your app, pass a function
|
799
frontend-new/src/pages/Acl/acl.jsx
Normal file
799
frontend-new/src/pages/Acl/acl.jsx
Normal file
@@ -0,0 +1,799 @@
|
||||
/*
|
||||
* Licensed to the Apache Software Foundation (ASF) under one or more
|
||||
* contributor license agreements. See the NOTICE file distributed with
|
||||
* this work for additional information regarding copyright ownership.
|
||||
* The ASF licenses this file to You under the Apache License, Version 2.0
|
||||
* (the "License"); you may not use this file except in compliance with
|
||||
* the License. You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import {
|
||||
Table,
|
||||
Button,
|
||||
Input,
|
||||
Tabs,
|
||||
Modal,
|
||||
Form,
|
||||
message,
|
||||
Space,
|
||||
Tag,
|
||||
Popconfirm,
|
||||
Select
|
||||
} from 'antd';
|
||||
import {
|
||||
EditOutlined,
|
||||
DeleteOutlined,
|
||||
EyeOutlined,
|
||||
EyeInvisibleOutlined
|
||||
} from '@ant-design/icons';
|
||||
import {remoteApi} from "../../api/remoteApi/remoteApi";
|
||||
import ResourceInput from '../../components/acl/ResourceInput';
|
||||
import SubjectInput from "../../components/acl/SubjectInput";
|
||||
import {useLanguage} from "../../i18n/LanguageContext";
|
||||
const { TabPane } = Tabs;
|
||||
const { Search } = Input;
|
||||
|
||||
const Acl = () => {
|
||||
const [activeTab, setActiveTab] = useState('users');
|
||||
const [userListData, setUserListData] = useState([]);
|
||||
const [aclListData, setAclListData] = useState([]);
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [searchText, setSearchText] = useState('');
|
||||
|
||||
const [isUserModalVisible, setIsUserModalVisible] = useState(false);
|
||||
const [currentUser, setCurrentUser] = useState(null);
|
||||
const [userForm] = Form.useForm();
|
||||
const [showPassword, setShowPassword] = useState(false);
|
||||
|
||||
const [isAclModalVisible, setIsAclModalVisible] = useState(false);
|
||||
const [currentAcl, setCurrentAcl] = useState(null);
|
||||
const [aclForm] = Form.useForm();
|
||||
const [messageApi, msgContextHolder] = message.useMessage();
|
||||
const [isUpdate, setIsUpdate] = useState(false);
|
||||
const [ips, setIps] = useState([]);
|
||||
const {t} = useLanguage();
|
||||
// 校验IP地址的正则表达式
|
||||
const ipRegex =
|
||||
/^(?:(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.){3}(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)$|^((?:[0-9A-Fa-f]{1,4}:){7}[0-9A-Fa-f]{1,4}|(?:[0-9A-Fa-f]{1,4}:){6}[0-9A-Fa-f]{1,4}(?::[0-9A-Fa-f]{1,4}){0,1}|(?:[0-9A-Fa-f]{1,4}:){5}[0-9A-Fa-f]{1,4}(?::[0-9A-Fa-f]{1,4}){0,2}|(?:[0-9A-Fa-f]{1,4}:){4}[0-9A-Fa-f]{1,4}(?::[0-9A-Fa-f]{1,4}){0,3}|(?:[0-9A-Fa-f]{1,4}:){3}[0-9A-Fa-f]{1,4}(?::[0-9A-Fa-f]{1,4}){0,4}|(?:[0-9A-Fa-f]{1,4}:){2}[0-9A-Fa-f]{1,4}(?::[0-9A-Fa-f]{1,4}){0,5}|(?:[0-9A-Fa-f]{1,4}:){1}[0-9A-Fa-f]{1,4}(?::[0-9A-Fa-f]{1,4}){0,6}|(?::(?::[0-9A-Fa-f]{1,4}){1,7}|::))(\/(?:12[0-7]|1[0-1][0-9]|[1-9]?[0-9]))?$/;
|
||||
// 支持 IPv4 和 IPv6,包括 CIDR 表示法
|
||||
// State to store the entire clusterInfo object for easy access
|
||||
const [clusterData, setClusterData] = useState(null);
|
||||
|
||||
// State for the list of available cluster names for the dropdown
|
||||
const [clusterNamesOptions, setClusterNamesOptions] = useState([]);
|
||||
|
||||
// State for the currently selected cluster name
|
||||
const [selectedCluster, setSelectedCluster] = useState(undefined);
|
||||
|
||||
// State for the list of available broker names for the dropdown (depends on selectedCluster)
|
||||
const [brokerNamesOptions, setBrokerNamesOptions] = useState([]);
|
||||
|
||||
// State for the currently selected broker name
|
||||
const [selectedBroker, setSelectedBroker] = useState(undefined);
|
||||
|
||||
// State for the address of the selected broker
|
||||
const [brokerAddress, setBrokerAddress] = useState(undefined);
|
||||
|
||||
// --- Data Fetching and Initial Setup ---
|
||||
useEffect(() => {
|
||||
const fetchData = async () => {
|
||||
const clusterResponse = await remoteApi.getClusterList();
|
||||
if (clusterResponse.status === 0 && clusterResponse.data) {
|
||||
const { clusterInfo } = clusterResponse.data;
|
||||
setClusterData(clusterInfo); // Store the entire clusterInfo
|
||||
|
||||
// Populate cluster names for the first dropdown
|
||||
const clusterNames = Object.keys(clusterInfo?.clusterAddrTable || {});
|
||||
setClusterNamesOptions(clusterNames.map(name => ({ label: name, value: name })));
|
||||
|
||||
// Set initial selections if clusters are available
|
||||
if (clusterNames.length > 0) {
|
||||
const defaultCluster = clusterNames[0];
|
||||
setSelectedCluster(defaultCluster);
|
||||
|
||||
// Manually trigger broker list update for the default cluster
|
||||
updateBrokerOptions(defaultCluster, clusterInfo);
|
||||
|
||||
// Set default broker and its address if available
|
||||
const brokersInDefaultCluster = clusterInfo.clusterAddrTable[defaultCluster] || [];
|
||||
if (brokersInDefaultCluster.length > 0) {
|
||||
const defaultBroker = brokersInDefaultCluster[0];
|
||||
setSelectedBroker(defaultBroker);
|
||||
// Get the address from brokerAddrTable using the defaultBroker name
|
||||
const addr = clusterInfo.brokerAddrTable?.[defaultBroker]?.brokerAddrs?.["0"];
|
||||
setBrokerAddress(addr);
|
||||
}
|
||||
}
|
||||
|
||||
} else {
|
||||
console.error('Failed to fetch cluster list:', clusterResponse.errMsg);
|
||||
}
|
||||
};
|
||||
if(!clusterData){
|
||||
fetchData();
|
||||
}
|
||||
if(brokerAddress){
|
||||
// Call fetchUsers or fetchAcls based on activeTab initially
|
||||
if (activeTab === 'users') {
|
||||
fetchUsers();
|
||||
} else {
|
||||
fetchAcls();
|
||||
}
|
||||
}
|
||||
|
||||
}, [activeTab]); // Dependencies for useEffect
|
||||
|
||||
// --- Helper function to update broker options based on selected cluster ---
|
||||
const updateBrokerOptions = (clusterName, info = clusterData) => {
|
||||
if (!info || !info.clusterAddrTable) {
|
||||
setBrokerNamesOptions([]);
|
||||
return;
|
||||
}
|
||||
const brokersInCluster = info.clusterAddrTable[clusterName] || [];
|
||||
setBrokerNamesOptions(brokersInCluster.map(broker => ({ label: broker, value: broker })));
|
||||
};
|
||||
|
||||
// --- Event Handlers ---
|
||||
const handleClusterChange = (value) => {
|
||||
setSelectedCluster(value);
|
||||
setSelectedBroker(undefined); // Reset broker selection
|
||||
setBrokerAddress(undefined); // Reset broker address
|
||||
|
||||
// Update the broker options based on the newly selected cluster
|
||||
updateBrokerOptions(value);
|
||||
};
|
||||
|
||||
const handleBrokerChange = (value) => {
|
||||
setSelectedBroker(value);
|
||||
// Find the corresponding broker address from clusterData
|
||||
if (clusterData && clusterData.brokerAddrTable && clusterData.brokerAddrTable[value]) {
|
||||
const addr = clusterData.brokerAddrTable[value].brokerAddrs?.["0"];
|
||||
setBrokerAddress(addr);
|
||||
} else {
|
||||
setBrokerAddress(undefined);
|
||||
}
|
||||
};
|
||||
|
||||
// --- Log selected values for debugging (optional) ---
|
||||
useEffect(() => {
|
||||
console.log('Selected Cluster:', selectedCluster);
|
||||
console.log('Selected Broker:', selectedBroker);
|
||||
console.log('Broker Address:', brokerAddress);
|
||||
}, [selectedCluster, selectedBroker, brokerAddress]);
|
||||
const handleIpChange = value => {
|
||||
// 过滤掉重复的IP地址
|
||||
const uniqueIps = Array.from(new Set(value));
|
||||
setIps(uniqueIps);
|
||||
};
|
||||
|
||||
const handleIpDeselect = value => {
|
||||
// 移除被取消选择的IP
|
||||
setIps(ips.filter(ip => ip !== value));
|
||||
};
|
||||
|
||||
const validateIp = (rule, value) => {
|
||||
if (!value || value.length === 0) {
|
||||
return Promise.resolve(); // Allow empty
|
||||
}
|
||||
const invalidIps = value.filter(ip => !ipRegex.test(ip));
|
||||
if (invalidIps.length > 0) {
|
||||
return Promise.reject(t.INVALID_IP_ADDRESSES +"ips:" + invalidIps.join(', '));
|
||||
}
|
||||
return Promise.resolve();
|
||||
};
|
||||
|
||||
// --- Data Loading Functions ---
|
||||
const fetchUsers = async () => {
|
||||
setLoading(true);
|
||||
try {
|
||||
const result = await remoteApi.listUsers(brokerAddress);
|
||||
if (result && result.status === 0 && result.data) {
|
||||
const formattedUsers = result.data.map(user => ({
|
||||
...user,
|
||||
key: user.username, // Table needs key
|
||||
userStatus: user.userStatus === 'enable' ? t.ENABLED : t.DISABLED // Format status
|
||||
}));
|
||||
setUserListData(formattedUsers);
|
||||
} else {
|
||||
messageApi.error(t.GET_USERS_FAILED+result?.errMsg);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("Failed to fetch users:", error);
|
||||
messageApi.error(t.GET_USERS_EXCEPTION);
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
const fetchAcls = async (value) => {
|
||||
setLoading(true);
|
||||
try {
|
||||
const result = await remoteApi.listAcls(brokerAddress, value);
|
||||
if (result && result.status === 0) {
|
||||
const formattedAcls = [];
|
||||
|
||||
if (result && result.data && Array.isArray(result.data)) {
|
||||
result.data.forEach((acl, aclIndex) => {
|
||||
const subject = acl.subject;
|
||||
|
||||
if (acl.policies && Array.isArray(acl.policies)) {
|
||||
acl.policies.forEach((policy, policyIndex) => {
|
||||
const policyType = policy.policyType;
|
||||
|
||||
if (policy.entries && Array.isArray(policy.entries)) {
|
||||
policy.entries.forEach((entry, entryIndex) => {
|
||||
const resources = Array.isArray(entry.resource) ? entry.resource : (entry.resource ? [entry.resource] : []);
|
||||
|
||||
resources.forEach((singleResource, resourceIndex) => {
|
||||
console.log(singleResource)
|
||||
formattedAcls.push({
|
||||
key: `acl-${aclIndex}-policy-${policyIndex}-entry-${entryIndex}-resource-${singleResource}`,
|
||||
subject: subject,
|
||||
policyType: policyType,
|
||||
resource: singleResource || t.N_A,
|
||||
actions: (entry.actions && Array.isArray(entry.actions)) ? entry.actions.join(', ') : '',
|
||||
sourceIps: (entry.sourceIps && Array.isArray(entry.sourceIps)) ? entry.sourceIps.join(', ') : t.N_A,
|
||||
decision: entry.decision || t.N_A
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
} else {
|
||||
console.warn(t.INVALID_OR_EMPTY_ACL_DATA);
|
||||
}
|
||||
setAclListData(formattedAcls);
|
||||
} else {
|
||||
messageApi.error(t.GET_ACLS_FAILED + result?.errMsg);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("Failed to fetch ACLs:", error);
|
||||
messageApi.error(t.GET_ACLS_EXCEPTION);
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
// --- User Management Logic ---
|
||||
|
||||
const handleAddUser = () => {
|
||||
setCurrentUser(null);
|
||||
userForm.resetFields();
|
||||
setShowPassword(false);
|
||||
setIsUserModalVisible(true);
|
||||
};
|
||||
|
||||
const handleEditUser = (record) => {
|
||||
setCurrentUser(record);
|
||||
userForm.setFieldsValue({
|
||||
username: record.username,
|
||||
password: record.password,
|
||||
userType: record.userType,
|
||||
userStatus: record.userStatus === t.ENABLED ? 'enable' : 'disable'
|
||||
});
|
||||
setShowPassword(false);
|
||||
setIsUserModalVisible(true);
|
||||
};
|
||||
|
||||
const handleDeleteUser = async (username) => {
|
||||
setLoading(true);
|
||||
try {
|
||||
const result = await remoteApi.deleteUser(brokerAddress, username);
|
||||
if (result.status === 0) {
|
||||
messageApi.success(t.USER_DELETE_SUCCESS);
|
||||
fetchUsers(brokerAddress);
|
||||
} else {
|
||||
messageApi.error(t.USER_DELETE_FAILED + result.errMsg);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("Failed to delete user:", error);
|
||||
messageApi.error(t.USER_DELETE_EXCEPTION);
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
const handleUserModalOk = async () => {
|
||||
try {
|
||||
const values = await userForm.validateFields();
|
||||
setLoading(true);
|
||||
let result;
|
||||
|
||||
const userInfoParam = {
|
||||
username: values.username,
|
||||
password: values.password,
|
||||
userType: values.userType,
|
||||
userStatus: values.userStatus,
|
||||
};
|
||||
|
||||
if (currentUser) {
|
||||
result = await remoteApi.updateUser(brokerAddress, userInfoParam);
|
||||
if (result.status === 0) {
|
||||
messageApi.success(t.USER_UPDATE_SUCCESS);
|
||||
} else {
|
||||
messageApi.error(result.errMsg);
|
||||
}
|
||||
} else {
|
||||
result = await remoteApi.createUser(brokerAddress, userInfoParam);
|
||||
if (result.status === 0) {
|
||||
messageApi.success(t.USER_CREATE_SUCCESS);
|
||||
} else {
|
||||
messageApi.error(result.errMsg);
|
||||
}
|
||||
}
|
||||
setIsUserModalVisible(false);
|
||||
fetchUsers();
|
||||
} catch (error) {
|
||||
console.error("Failed to save user:", error);
|
||||
messageApi.error(t.SAVE_USER_FAILED);
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
// --- ACL Permission Management Logic ---
|
||||
const handleAddAcl = () => {
|
||||
setCurrentAcl(null);
|
||||
setIsUpdate(false)
|
||||
aclForm.resetFields();
|
||||
setIsAclModalVisible(true);
|
||||
};
|
||||
|
||||
const handleEditAcl = (record) => {
|
||||
setCurrentAcl(record);
|
||||
setIsUpdate(true);
|
||||
aclForm.setFieldsValue({
|
||||
subject: record.subject,
|
||||
policyType: record.policyType,
|
||||
resource: record.resource,
|
||||
actions: record.actions ? record.actions.split(', ') : [],
|
||||
sourceIps: record.sourceIps ? record.sourceIps.split(', ') : [],
|
||||
decision: record.decision
|
||||
});
|
||||
setIsAclModalVisible(true);
|
||||
};
|
||||
|
||||
const handleDeleteAcl = async (subject, resource) => {
|
||||
setLoading(true);
|
||||
try {
|
||||
const result = await remoteApi.deleteAcl(brokerAddress, subject, resource);
|
||||
if (result.status === 0) {
|
||||
messageApi.success(t.ACL_DELETE_SUCCESS);
|
||||
fetchAcls();
|
||||
} else {
|
||||
messageApi.error(t.ACL_DELETE_FAILED+result.errMsg);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("Failed to delete ACL:", error);
|
||||
messageApi.error(t.ACL_DELETE_EXCEPTION);
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
const handleAclModalOk = async () => {
|
||||
try {
|
||||
const values = await aclForm.validateFields();
|
||||
setLoading(true);
|
||||
let result;
|
||||
|
||||
const policiesParam = [
|
||||
{
|
||||
policyType: values.policyType,
|
||||
entries: [
|
||||
{
|
||||
resource: isUpdate ? [values.resource] : values.resource,
|
||||
actions: values.actions,
|
||||
sourceIps: values.sourceIps,
|
||||
decision: values.decision
|
||||
}
|
||||
]
|
||||
}
|
||||
];
|
||||
|
||||
if (isUpdate) { // This condition seems reversed for update/create based on the current logic.
|
||||
result = await remoteApi.updateAcl(brokerAddress, values.subject, policiesParam);
|
||||
if (result.status === 0) {
|
||||
messageApi.success(t.ACL_UPDATE_SUCCESS);
|
||||
setIsAclModalVisible(false);
|
||||
fetchAcls(brokerAddress);
|
||||
} else {
|
||||
messageApi.error(t.ACL_UPDATE_FAILED+result.errMsg);
|
||||
}
|
||||
setIsUpdate(false)
|
||||
} else {
|
||||
result = await remoteApi.createAcl(brokerAddress, values.subject, policiesParam);
|
||||
console.log(result)
|
||||
if (result.status === 0) {
|
||||
messageApi.success(t.ACL_CREATE_SUCCESS);
|
||||
setIsAclModalVisible(false);
|
||||
fetchAcls(brokerAddress);
|
||||
} else {
|
||||
messageApi.error(t.ACL_CREATE_FAILED+result.errMsg);
|
||||
}
|
||||
}
|
||||
|
||||
} catch (error) {
|
||||
console.error("Failed to save ACL:", error);
|
||||
messageApi.error(t.SAVE_ACL_FAILED);
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
// --- Search Functionality ---
|
||||
|
||||
const handleSearch = (value) => {
|
||||
if (activeTab === 'users') {
|
||||
const filteredData = userListData.filter(item =>
|
||||
Object.values(item).some(val =>
|
||||
String(val).toLowerCase().includes(value.toLowerCase())
|
||||
)
|
||||
);
|
||||
if (value === '') {
|
||||
fetchUsers();
|
||||
} else {
|
||||
setUserListData(filteredData);
|
||||
}
|
||||
} else {
|
||||
fetchAcls(value);
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
// --- User Table Column Definitions ---
|
||||
const userColumns = [
|
||||
{
|
||||
title: t.USERNAME,
|
||||
dataIndex: 'username',
|
||||
key: 'username',
|
||||
},
|
||||
{
|
||||
title: t.PASSWORD,
|
||||
dataIndex: 'password',
|
||||
key: 'password',
|
||||
render: (text) => (
|
||||
<span>
|
||||
{showPassword ? text : '********'}
|
||||
<Button
|
||||
type="link"
|
||||
icon={showPassword ? <EyeInvisibleOutlined /> : <EyeOutlined />}
|
||||
onClick={() => setShowPassword(!showPassword)}
|
||||
style={{ marginLeft: 8 }}
|
||||
>
|
||||
{showPassword ? t.HIDE : t.VIEW}
|
||||
</Button>
|
||||
</span>
|
||||
),
|
||||
},
|
||||
{
|
||||
title: t.USER_TYPE,
|
||||
dataIndex: 'userType',
|
||||
key: 'userType',
|
||||
},
|
||||
{
|
||||
title: t.USER_STATUS,
|
||||
dataIndex: 'userStatus',
|
||||
key: 'userStatus',
|
||||
render: (status) => (
|
||||
<Tag color={status=== 'enable' ? 'green' : 'red'}>{status}</Tag>
|
||||
),
|
||||
},
|
||||
{
|
||||
title: t.OPERATION,
|
||||
key: 'action',
|
||||
render: (_, record) => (
|
||||
<Space size="middle">
|
||||
<Button icon={<EditOutlined />} onClick={() => handleEditUser(record)}>{t.MODIFY}</Button>
|
||||
<Popconfirm
|
||||
title={t.CONFIRM_DELETE_USER}
|
||||
onConfirm={() => handleDeleteUser(record.username)}
|
||||
okText={t.YES}
|
||||
cancelText={t.NO}
|
||||
>
|
||||
<Button icon={<DeleteOutlined />} danger>{t.DELETE}</Button>
|
||||
</Popconfirm>
|
||||
</Space>
|
||||
),
|
||||
},
|
||||
];
|
||||
|
||||
// --- ACL Permission Table Column Definitions ---
|
||||
const aclColumns = [
|
||||
{
|
||||
title: t.USERNAME_SUBJECT,
|
||||
dataIndex: 'subject',
|
||||
key: 'subject',
|
||||
},
|
||||
{
|
||||
title: t.POLICY_TYPE,
|
||||
dataIndex: 'policyType',
|
||||
key: 'policyType',
|
||||
},
|
||||
{
|
||||
title: t.RESOURCE_NAME,
|
||||
dataIndex: 'resource',
|
||||
key: 'resource',
|
||||
},
|
||||
{
|
||||
title: t.OPERATION_TYPE,
|
||||
dataIndex: 'actions',
|
||||
key: 'actions',
|
||||
render: (text) => text ? text.split(', ').map((action, index) => (
|
||||
<Tag key={index} color="blue">{action}</Tag>
|
||||
)) : null,
|
||||
},
|
||||
{
|
||||
title: t.SOURCE_IP,
|
||||
dataIndex: 'sourceIps',
|
||||
key: 'sourceIps',
|
||||
},
|
||||
{
|
||||
title: t.DECISION,
|
||||
dataIndex: 'decision',
|
||||
key: 'decision',
|
||||
render: (text) => (
|
||||
<Tag color={text === 'Allow' ? 'green' : 'red'}>{text}</Tag>
|
||||
),
|
||||
},
|
||||
{
|
||||
title: t.OPERATION,
|
||||
key: 'action',
|
||||
render: (_, record) => (
|
||||
<Space size="middle">
|
||||
<Button icon={<EditOutlined />} onClick={() => handleEditAcl(record)}>{t.MODIFY}</Button>
|
||||
<Popconfirm
|
||||
title={t.CONFIRM_DELETE_ACL}
|
||||
onConfirm={() => handleDeleteAcl(record.subject, record.resource)}
|
||||
okText={t.YES}
|
||||
cancelText={t.NO}
|
||||
>
|
||||
<Button icon={<DeleteOutlined />} danger>{t.DELETE}</Button>
|
||||
</Popconfirm>
|
||||
</Space>
|
||||
),
|
||||
},
|
||||
];
|
||||
|
||||
return (
|
||||
<>
|
||||
{msgContextHolder}
|
||||
<div style={{padding: 24}}>
|
||||
<h2>{t.ACL_MANAGEMENT}</h2>
|
||||
|
||||
<div style={{ marginBottom: 16, display: 'flex', gap: 16 }}>
|
||||
<Form.Item label={t.PLEASE_SELECT_CLUSTER} style={{ marginBottom: 0 }}>
|
||||
<Select
|
||||
placeholder={t.PLEASE_SELECT_CLUSTER}
|
||||
style={{ width: 200 }}
|
||||
onChange={handleClusterChange}
|
||||
value={selectedCluster}
|
||||
options={clusterNamesOptions}
|
||||
/>
|
||||
</Form.Item>
|
||||
<Form.Item label={t.PLEASE_SELECT_BROKER} style={{ marginBottom: 0 }}>
|
||||
<Select
|
||||
placeholder={t.PLEASE_SELECT_BROKER}
|
||||
style={{ width: 200 }}
|
||||
onChange={handleBrokerChange}
|
||||
value={selectedBroker}
|
||||
options={brokerNamesOptions} // Now dynamically updated
|
||||
disabled={!selectedCluster} // Disable broker selection if no cluster is chosen
|
||||
/>
|
||||
</Form.Item>
|
||||
<Button type="primary" onClick={activeTab === 'users' ? fetchUsers : fetchAcls}>
|
||||
{t.CONFIRM}
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
<Tabs activeKey={activeTab} onChange={setActiveTab}>
|
||||
<TabPane tab={t.ACL_USERS} key="users"/>
|
||||
<TabPane tab={t.ACL_PERMISSIONS} key="acls"/>
|
||||
</Tabs>
|
||||
|
||||
<div style={{marginBottom: 16, display: 'flex', justifyContent: 'space-between'}}>
|
||||
<Button type="primary" onClick={activeTab === 'users' ? handleAddUser : handleAddAcl}>
|
||||
{activeTab === 'users' ? t.ADD_USER : t.ADD_ACL_PERMISSION}
|
||||
</Button>
|
||||
<Search
|
||||
placeholder={t.SEARCH_PLACEHOLDER}
|
||||
allowClear
|
||||
onSearch={handleSearch}
|
||||
style={{width: 300}}
|
||||
/>
|
||||
</div>
|
||||
|
||||
{activeTab === 'users' && (
|
||||
<Table
|
||||
columns={userColumns}
|
||||
dataSource={userListData}
|
||||
loading={loading}
|
||||
pagination={{pageSize: 10}}
|
||||
rowKey="username"
|
||||
/>
|
||||
)}
|
||||
|
||||
{activeTab === 'acls' && (
|
||||
<Table
|
||||
columns={aclColumns}
|
||||
dataSource={aclListData}
|
||||
loading={loading}
|
||||
pagination={{pageSize: 10}}
|
||||
rowKey="key"
|
||||
/>
|
||||
)}
|
||||
|
||||
{/* User Management Modal */}
|
||||
<Modal
|
||||
title={currentUser ? t.EDIT_USER : t.ADD_USER}
|
||||
visible={isUserModalVisible}
|
||||
onOk={handleUserModalOk}
|
||||
onCancel={() => setIsUserModalVisible(false)}
|
||||
confirmLoading={loading}
|
||||
footer={[
|
||||
<Button key="cancel" onClick={() => setIsUserModalVisible(false)}>
|
||||
{t.CANCEL}
|
||||
</Button>,
|
||||
<Button key="submit" type="primary" onClick={handleUserModalOk} loading={loading}>
|
||||
{t.CONFIRM}
|
||||
</Button>,
|
||||
]}
|
||||
>
|
||||
<Form
|
||||
form={userForm}
|
||||
layout="vertical"
|
||||
name="user_form"
|
||||
initialValues={{userStatus: 'enable'}}
|
||||
>
|
||||
<Form.Item
|
||||
name="username"
|
||||
label={t.USERNAME}
|
||||
rules={[{required: true, message: t.PLEASE_ENTER_USERNAME}]}
|
||||
>
|
||||
<Input disabled={!!currentUser}/>
|
||||
</Form.Item>
|
||||
<Form.Item
|
||||
name="password"
|
||||
label={t.PASSWORD}
|
||||
rules={[{required: !currentUser, message: t.PLEASE_ENTER_PASSWORD}]}
|
||||
>
|
||||
<Input.Password
|
||||
placeholder={t.PASSWORD}
|
||||
iconRender={visible => (visible ? <EyeOutlined/> : <EyeInvisibleOutlined/>)}
|
||||
/>
|
||||
</Form.Item>
|
||||
<Form.Item
|
||||
name="userType"
|
||||
label={t.USER_TYPE}
|
||||
rules={[{required: true, message: t.PLEASE_SELECT_USER_TYPE}]}
|
||||
>
|
||||
<Select mode="single" placeholder="Super, Normal" style={{width: '100%'}}>
|
||||
<Select.Option value="Super">Super</Select.Option>
|
||||
<Select.Option value="Normal">Normal</Select.Option>
|
||||
</Select>
|
||||
</Form.Item>
|
||||
<Form.Item
|
||||
name="userStatus"
|
||||
label={t.USER_STATUS}
|
||||
rules={[{required: true, message: t.PLEASE_SELECT_USER_STATUS}]}
|
||||
>
|
||||
<Select mode="single" placeholder="enable, disable" style={{width: '100%'}}>
|
||||
<Select.Option value="enable">enable</Select.Option>
|
||||
<Select.Option value="disable">disable</Select.Option>
|
||||
</Select>
|
||||
</Form.Item>
|
||||
</Form>
|
||||
</Modal>
|
||||
|
||||
{/* ACL Permission Management Modal */}
|
||||
<Modal
|
||||
title={currentAcl ? t.EDIT_ACL_PERMISSION : t.ADD_ACL_PERMISSION}
|
||||
visible={isAclModalVisible}
|
||||
onOk={handleAclModalOk}
|
||||
onCancel={() => setIsAclModalVisible(false)}
|
||||
confirmLoading={loading}
|
||||
>
|
||||
<Form
|
||||
form={aclForm}
|
||||
layout="vertical"
|
||||
name="acl_form"
|
||||
>
|
||||
<Form.Item
|
||||
name="subject"
|
||||
label={t.SUBJECT_LABEL}
|
||||
rules={[{required: true, message: t.PLEASE_ENTER_SUBJECT}]}
|
||||
>
|
||||
<SubjectInput disabled={!!currentAcl}/>
|
||||
</Form.Item>
|
||||
|
||||
<Form.Item
|
||||
name="policyType"
|
||||
label={t.POLICY_TYPE}
|
||||
rules={[{required: true, message: t.PLEASE_ENTER_POLICY_TYPE}]}
|
||||
>
|
||||
<Select mode="single" disabled={isUpdate} placeholder="policyType" style={{width: '100%'}}>
|
||||
<Select.Option value="Custom">Custom</Select.Option>
|
||||
<Select.Option value="Default">Default</Select.Option>
|
||||
</Select>
|
||||
</Form.Item>
|
||||
|
||||
<Form.Item
|
||||
name="resource"
|
||||
label={t.RESOURCE}
|
||||
rules={[{required: true, message: t.PLEASE_ADD_RESOURCE}]}
|
||||
>
|
||||
{isUpdate ? (
|
||||
<Input disabled={isUpdate}/>
|
||||
) : (
|
||||
<ResourceInput/>
|
||||
)}
|
||||
</Form.Item>
|
||||
|
||||
<Form.Item
|
||||
name="actions"
|
||||
label={t.OPERATION_TYPE}
|
||||
>
|
||||
<Select mode="multiple" placeholder="action" style={{width: '100%'}}>
|
||||
<Select.Option value="All">All</Select.Option>
|
||||
<Select.Option value="Pub">Pub</Select.Option>
|
||||
<Select.Option value="Sub">Sub</Select.Option>
|
||||
<Select.Option value="Create">Create</Select.Option>
|
||||
<Select.Option value="Update">Update</Select.Option>
|
||||
<Select.Option value="Delete">Delete</Select.Option>
|
||||
<Select.Option value="Get">Get</Select.Option>
|
||||
<Select.Option value="List">List</Select.Option>
|
||||
</Select>
|
||||
</Form.Item>
|
||||
<Form.Item
|
||||
name="sourceIps"
|
||||
label={t.SOURCE_IP}
|
||||
rules={[
|
||||
{
|
||||
validator: validateIp,
|
||||
},
|
||||
]}
|
||||
>
|
||||
<Select
|
||||
mode="tags"
|
||||
style={{width: '100%'}}
|
||||
placeholder={t.ENTER_IP_HINT}
|
||||
onChange={handleIpChange}
|
||||
onDeselect={handleIpDeselect}
|
||||
value={ips}
|
||||
tokenSeparators={[',', ' ']}
|
||||
>
|
||||
<Select.Option value="192.168.1.1">192.168.1.1</Select.Option>
|
||||
<Select.Option value="0.0.0.0">0.0.0.0</Select.Option>
|
||||
<Select.Option value="127.0.0.1">127.0.0.1</Select.Option>
|
||||
</Select>
|
||||
</Form.Item>
|
||||
<Form.Item
|
||||
name="decision"
|
||||
label={t.DECISION}
|
||||
rules={[{required: true, message: t.PLEASE_ENTER_DECISION}]}
|
||||
>
|
||||
<Select mode="single" placeholder="Allow, Deny" style={{width: '100%'}}>
|
||||
<Select.Option value="Allow">Allow</Select.Option>
|
||||
<Select.Option value="Deny">Deny</Select.Option>
|
||||
</Select>
|
||||
</Form.Item>
|
||||
</Form>
|
||||
</Modal>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
export default Acl;
|
303
frontend-new/src/pages/Cluster/cluster.jsx
Normal file
303
frontend-new/src/pages/Cluster/cluster.jsx
Normal file
@@ -0,0 +1,303 @@
|
||||
/*
|
||||
* Licensed to the Apache Software Foundation (ASF) under one or more
|
||||
* contributor license agreements. See the NOTICE file distributed with
|
||||
* this work for additional information regarding copyright ownership.
|
||||
* The ASF licenses this file to You under the Apache License, Version 2.0
|
||||
* (the "License"); you may not use this file except in compliance with
|
||||
* the License. You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import React, {useCallback, useEffect, useState} from 'react';
|
||||
import {Button, Modal, notification, Select, Spin, Table} from 'antd';
|
||||
import {useLanguage} from "../../i18n/LanguageContext";
|
||||
import {remoteApi, tools} from "../../api/remoteApi/remoteApi"; // 确保路径正确
|
||||
|
||||
const {Option} = Select;
|
||||
|
||||
const Cluster = () => {
|
||||
const {t} = useLanguage();
|
||||
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [clusterNames, setClusterNames] = useState([]);
|
||||
const [selectedCluster, setSelectedCluster] = useState('');
|
||||
const [instances, setInstances] = useState([]);
|
||||
const [allBrokersData, setAllBrokersData] = useState({});
|
||||
|
||||
const [detailModalVisible, setDetailModalVisible] = useState(false);
|
||||
const [configModalVisible, setConfigModalVisible] = useState(false);
|
||||
const [currentDetail, setCurrentDetail] = useState({});
|
||||
const [currentConfig, setCurrentConfig] = useState({});
|
||||
const [currentBrokerName, setCurrentBrokerName] = useState('');
|
||||
const [currentIndex, setCurrentIndex] = useState(null); // 对应 brokerId
|
||||
const [currentBrokerAddress, setCurrentBrokerAddress] = useState('');
|
||||
const [api, contextHolder] = notification.useNotification();
|
||||
|
||||
const switchCluster = useCallback((clusterName) => {
|
||||
if (allBrokersData[clusterName]) {
|
||||
setInstances(allBrokersData[clusterName]);
|
||||
} else {
|
||||
setInstances([]);
|
||||
}
|
||||
}, [allBrokersData]);
|
||||
|
||||
const handleChangeCluster = (value) => {
|
||||
setSelectedCluster(value);
|
||||
switchCluster(value);
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
setLoading(true);
|
||||
remoteApi.queryClusterList((resp) => {
|
||||
setLoading(false);
|
||||
if (resp.status === 0) {
|
||||
const {clusterInfo, brokerServer} = resp.data;
|
||||
const {clusterAddrTable, brokerAddrTable} = clusterInfo;
|
||||
|
||||
const generatedBrokers = tools.generateBrokerMap(brokerServer, clusterAddrTable, brokerAddrTable);
|
||||
setAllBrokersData(generatedBrokers);
|
||||
|
||||
const names = Object.keys(clusterAddrTable);
|
||||
setClusterNames(names);
|
||||
|
||||
if (names.length > 0) {
|
||||
const defaultCluster = names[0];
|
||||
setSelectedCluster(defaultCluster);
|
||||
if (generatedBrokers[defaultCluster]) {
|
||||
setInstances(generatedBrokers[defaultCluster]);
|
||||
} else {
|
||||
setInstances([]);
|
||||
}
|
||||
}
|
||||
|
||||
} else {
|
||||
api.error({message: resp.errMsg || t.QUERY_CLUSTER_LIST_FAILED, duration: 2});
|
||||
}
|
||||
});
|
||||
}, []);
|
||||
|
||||
const showDetail = (brokerName, brokerId, record) => { // 传入 record 整个对象,方便直接显示
|
||||
setCurrentBrokerName(brokerName);
|
||||
setCurrentIndex(brokerId);
|
||||
setCurrentDetail(record); // 直接使用 record 作为详情
|
||||
setDetailModalVisible(true);
|
||||
};
|
||||
|
||||
const showConfig = (brokerAddress, brokerName, brokerId) => { // 保持一致,传入 brokerId
|
||||
setCurrentBrokerName(brokerName);
|
||||
setCurrentIndex(brokerId);
|
||||
setCurrentBrokerAddress(brokerAddress);
|
||||
|
||||
setLoading(true);
|
||||
remoteApi.queryBrokerConfig(brokerAddress, (resp) => {
|
||||
setLoading(false);
|
||||
if (resp.status === 0) {
|
||||
// ✨ 确保 resp.data 是一个对象,如果后端返回的不是对象,这里需要处理
|
||||
if (typeof resp.data === 'object' && resp.data !== null) {
|
||||
setCurrentConfig(resp.data);
|
||||
setConfigModalVisible(true);
|
||||
} else {
|
||||
api.error({message: t.INVALID_CONFIG_DATA || 'Invalid config data received', duration: 2});
|
||||
setCurrentConfig({}); // 清空配置,避免显示错误
|
||||
}
|
||||
} else {
|
||||
api.error({message: resp.errMsg || t.QUERY_BROKER_CONFIG_FAILED, duration: 2});
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
const columns = [
|
||||
{
|
||||
title: t.SPLIT,
|
||||
dataIndex: 'brokerName', // 直接使用 brokerId
|
||||
key: 'split',
|
||||
align: 'center'
|
||||
},
|
||||
{
|
||||
title: t.NO,
|
||||
dataIndex: 'brokerId', // 直接使用 brokerId
|
||||
key: 'no',
|
||||
align: 'center',
|
||||
render: (brokerId) => `${brokerId}${brokerId === 0 ? `(${t.MASTER})` : `(${t.SLAVE})`}`,
|
||||
},
|
||||
{
|
||||
title: t.ADDRESS,
|
||||
dataIndex: 'address', // 确保 generateBrokerMap 返回的数据有 address 字段
|
||||
key: 'address',
|
||||
align: 'center',
|
||||
},
|
||||
{
|
||||
title: t.VERSION,
|
||||
dataIndex: 'brokerVersionDesc',
|
||||
key: 'version',
|
||||
align: 'center',
|
||||
},
|
||||
{
|
||||
title: t.PRO_MSG_TPS,
|
||||
dataIndex: 'putTps',
|
||||
key: 'putTps',
|
||||
align: 'center',
|
||||
render: (text) => {
|
||||
const tpsValue = text ? Number(String(text).split(' ')[0]) : 0; // 确保text是字符串
|
||||
return tpsValue.toFixed(2);
|
||||
},
|
||||
},
|
||||
{
|
||||
title: t.CUS_MSG_TPS,
|
||||
key: 'cusMsgTps',
|
||||
align: 'center',
|
||||
render: (_, record) => {
|
||||
// 根据你提供的数据结构,这里可能是 getTransferredTps
|
||||
const val = record.getTransferedTps?.trim() ? record.getTransferedTps : record.getTransferredTps;
|
||||
const tpsValue = val ? Number(String(val).split(' ')[0]) : 0; // 确保val是字符串
|
||||
return tpsValue.toFixed(2);
|
||||
},
|
||||
},
|
||||
{
|
||||
title: t.YESTERDAY_PRO_COUNT,
|
||||
key: 'yesterdayProCount',
|
||||
align: 'center',
|
||||
render: (_, record) => {
|
||||
const putTotalTodayMorning = parseFloat(record.msgPutTotalTodayMorning || 0);
|
||||
const putTotalYesterdayMorning = parseFloat(record.msgPutTotalYesterdayMorning || 0);
|
||||
return (putTotalTodayMorning - putTotalYesterdayMorning).toLocaleString();
|
||||
}
|
||||
},
|
||||
{
|
||||
title: t.YESTERDAY_CUS_COUNT,
|
||||
key: 'yesterdayCusCount',
|
||||
align: 'center',
|
||||
render: (_, record) => {
|
||||
const getTotalTodayMorning = parseFloat(record.msgGetTotalTodayMorning || 0);
|
||||
const getTotalYesterdayMorning = parseFloat(record.msgGetTotalYesterdayMorning || 0);
|
||||
return (getTotalTodayMorning - getTotalYesterdayMorning).toLocaleString();
|
||||
}
|
||||
},
|
||||
{
|
||||
title: t.TODAY_PRO_COUNT,
|
||||
key: 'todayProCount',
|
||||
align: 'center',
|
||||
render: (_, record) => {
|
||||
const putTotalTodayNow = parseFloat(record.msgPutTotalTodayNow || 0);
|
||||
const putTotalTodayMorning = parseFloat(record.msgPutTotalTodayMorning || 0);
|
||||
return (putTotalTodayNow - putTotalTodayMorning).toLocaleString();
|
||||
}
|
||||
},
|
||||
{
|
||||
title: t.TODAY_CUS_COUNT,
|
||||
key: 'todayCusCount',
|
||||
align: 'center',
|
||||
render: (_, record) => {
|
||||
const getTotalTodayNow = parseFloat(record.msgGetTotalTodayNow || 0);
|
||||
const getTotalTodayMorning = parseFloat(record.msgGetTotalTodayMorning || 0);
|
||||
return (getTotalTodayNow - getTotalTodayMorning).toLocaleString();
|
||||
}
|
||||
},
|
||||
{
|
||||
title: t.OPERATION,
|
||||
key: 'operation',
|
||||
align: 'center',
|
||||
render: (_, record) => (
|
||||
<>
|
||||
<Button size="small" type="primary"
|
||||
onClick={() => showDetail(record.brokerName, record.brokerId, record)}
|
||||
style={{marginRight: 8}}>
|
||||
{t.STATUS}
|
||||
</Button>
|
||||
{/* 传入 record.address */}
|
||||
<Button size="small" type="primary"
|
||||
onClick={() => showConfig(record.address, record.brokerName, record.brokerId)}>
|
||||
{t.CONFIG}
|
||||
</Button>
|
||||
</>
|
||||
),
|
||||
},
|
||||
];
|
||||
|
||||
return (
|
||||
<>
|
||||
{contextHolder}
|
||||
<Spin spinning={loading} tip={t.LOADING}>
|
||||
<div style={{padding: 24}}>
|
||||
<div style={{marginBottom: 16, display: 'flex', alignItems: 'center'}}>
|
||||
<label style={{marginRight: 8}}>{t.CLUSTER}:</label>
|
||||
<Select
|
||||
style={{width: 300}}
|
||||
placeholder={t.SELECT_CLUSTER || "Please select a cluster"}
|
||||
value={selectedCluster}
|
||||
onChange={handleChangeCluster}
|
||||
allowClear
|
||||
>
|
||||
{clusterNames.map((name) => (
|
||||
<Option key={name} value={name}>
|
||||
{name}
|
||||
</Option>
|
||||
))}
|
||||
</Select>
|
||||
</div>
|
||||
|
||||
<Table
|
||||
dataSource={instances}
|
||||
columns={columns}
|
||||
rowKey={(record) => `${record.brokerName}-${record.brokerId}`}
|
||||
pagination={false}
|
||||
bordered
|
||||
size="middle"
|
||||
/>
|
||||
|
||||
<Modal
|
||||
title={`${t.BROKER} [${currentBrokerName}][${currentIndex}]`}
|
||||
open={detailModalVisible}
|
||||
footer={null}
|
||||
onCancel={() => setDetailModalVisible(false)}
|
||||
width={800}
|
||||
bodyStyle={{maxHeight: '60vh', overflowY: 'auto'}}
|
||||
>
|
||||
<Table
|
||||
dataSource={Object.entries(currentDetail).map(([key, value]) => ({key, value}))}
|
||||
columns={[
|
||||
{title: t.KEY || 'Key', dataIndex: 'key', key: 'key'},
|
||||
{title: t.VALUE || 'Value', dataIndex: 'value', key: 'value'},
|
||||
]}
|
||||
pagination={false}
|
||||
size="small"
|
||||
bordered
|
||||
rowKey="key"
|
||||
/>
|
||||
</Modal>
|
||||
|
||||
<Modal
|
||||
title={`${t.BROKER} [${currentBrokerName}][${currentIndex}]`}
|
||||
open={configModalVisible}
|
||||
footer={null}
|
||||
onCancel={() => setConfigModalVisible(false)}
|
||||
width={800}
|
||||
bodyStyle={{maxHeight: '60vh', overflowY: 'auto'}}
|
||||
>
|
||||
<Table
|
||||
dataSource={Object.entries(currentConfig).map(([key, value]) => ({key, value}))}
|
||||
columns={[
|
||||
{title: t.KEY || 'Key', dataIndex: 'key', key: 'key'},
|
||||
{title: t.VALUE || 'Value', dataIndex: 'value', key: 'value'},
|
||||
]}
|
||||
pagination={false}
|
||||
size="small"
|
||||
bordered
|
||||
rowKey="key"
|
||||
/>
|
||||
</Modal>
|
||||
</div>
|
||||
</Spin>
|
||||
</>
|
||||
|
||||
);
|
||||
};
|
||||
|
||||
export default Cluster;
|
480
frontend-new/src/pages/Consumer/consumer.jsx
Normal file
480
frontend-new/src/pages/Consumer/consumer.jsx
Normal file
@@ -0,0 +1,480 @@
|
||||
/*
|
||||
* Licensed to the Apache Software Foundation (ASF) under one or more
|
||||
* contributor license agreements. See the NOTICE file distributed with
|
||||
* this work for additional information regarding copyright ownership.
|
||||
* The ASF licenses this file to You under the Apache License, Version 2.0
|
||||
* (the "License"); you may not use this file except in compliance with
|
||||
* the License. You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import React, {useCallback, useEffect, useState} from 'react';
|
||||
import {Button, Checkbox, Input, message, notification, Spin, Table} from 'antd';
|
||||
import {useLanguage} from '../../i18n/LanguageContext';
|
||||
import {remoteApi} from '../../api/remoteApi/remoteApi';
|
||||
import ClientInfoModal from "../../components/consumer/ClientInfoModal";
|
||||
import ConsumerDetailModal from "../../components/consumer/ConsumerDetailModal";
|
||||
import ConsumerConfigModal from "../../components/consumer/ConsumerConfigModal";
|
||||
import DeleteConsumerModal from "../../components/consumer/DeleteConsumerModal";
|
||||
|
||||
const ConsumerGroupList = () => {
|
||||
const {t} = useLanguage();
|
||||
const [filterStr, setFilterStr] = useState('');
|
||||
const [filterNormal, setFilterNormal] = useState(true);
|
||||
const [filterFIFO, setFilterFIFO] = useState(false);
|
||||
const [filterSystem, setFilterSystem] = useState(false);
|
||||
const [rmqVersion, setRmqVersion] = useState(true);
|
||||
const [writeOperationEnabled, setWriteOperationEnabled] = useState(true);
|
||||
const [intervalProcessSwitch, setIntervalProcessSwitch] = useState(false);
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [consumerGroupShowList, setConsumerGroupShowList] = useState([]);
|
||||
const [allConsumerGroupList, setAllConsumerGroupList] = useState([]);
|
||||
const [selectedGroup, setSelectedGroup] = useState(null);
|
||||
const [selectedAddress, setSelectedAddress] = useState(null);
|
||||
const [showClientInfo, setShowClientInfo] = useState(false);
|
||||
const [showConsumeDetail, setShowConsumeDetail] = useState(false);
|
||||
const [showConfig, setShowConfig] = useState(false);
|
||||
const [isAddConfig, setIsAddConfig] = useState(false);
|
||||
const [showDeleteModal, setShowDeleteModal] = useState(false);
|
||||
const [messageApi, msgContextHolder] = message.useMessage();
|
||||
const [notificationApi,notificationContextHolder] = notification.useNotification();
|
||||
|
||||
const [paginationConf, setPaginationConf] = useState({
|
||||
current: 1,
|
||||
pageSize: 10,
|
||||
total: 0,
|
||||
});
|
||||
|
||||
const [sortConfig, setSortConfig] = useState({
|
||||
sortKey: null,
|
||||
sortOrder: 1,
|
||||
});
|
||||
|
||||
const loadConsumerGroups = useCallback(async (currentPage) => {
|
||||
setLoading(true);
|
||||
try {
|
||||
const response = await remoteApi.queryConsumerGroupList(false);
|
||||
if (response.status === 0) {
|
||||
setAllConsumerGroupList(response.data);
|
||||
if(currentPage!=null){
|
||||
filterList(currentPage, response.data);
|
||||
}else{
|
||||
filterList(1, response.data);
|
||||
}
|
||||
} else {
|
||||
messageApi.error({title: t.ERROR, content: response.errMsg});
|
||||
}
|
||||
} catch (error) {
|
||||
messageApi.error({title: t.ERROR, content: t.FAILED_TO_FETCH_DATA});
|
||||
console.error("Error loading consumer groups:", error);
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
}, [t]);
|
||||
|
||||
const filterByType = (str, type, version) => {
|
||||
if (filterSystem && type === "SYSTEM") return true;
|
||||
if (filterNormal && (type === "NORMAL" || (!version && type === "FIFO"))) return true;
|
||||
if (filterFIFO && type === "FIFO") return true;
|
||||
return false;
|
||||
};
|
||||
|
||||
const filterList = useCallback((currentPage, data) => {
|
||||
// 排序处理
|
||||
let sortedData = [...data];
|
||||
if (sortConfig.sortKey) {
|
||||
sortedData.sort((a, b) => {
|
||||
const aValue = a[sortConfig.sortKey];
|
||||
const bValue = b[sortConfig.sortKey];
|
||||
if (typeof aValue === 'string') {
|
||||
return sortConfig.sortOrder * aValue.localeCompare(bValue);
|
||||
}
|
||||
return sortConfig.sortOrder * (aValue > bValue ? 1 : -1);
|
||||
});
|
||||
}
|
||||
|
||||
// 过滤处理
|
||||
const lowExceptStr = filterStr.toLowerCase();
|
||||
const canShowList = sortedData.filter(element =>
|
||||
filterByType(element.group, element.subGroupType, rmqVersion) &&
|
||||
element.group.toLowerCase().includes(lowExceptStr)
|
||||
);
|
||||
|
||||
// 更新分页和显示列表
|
||||
const perPage = paginationConf.pageSize;
|
||||
const from = (currentPage - 1) * perPage;
|
||||
const to = from + perPage;
|
||||
|
||||
setPaginationConf(prev => ({
|
||||
...prev,
|
||||
current: currentPage,
|
||||
total: canShowList.length,
|
||||
}));
|
||||
setConsumerGroupShowList(canShowList.slice(from, to));
|
||||
}, [filterStr, filterNormal, filterSystem, filterFIFO, rmqVersion, sortConfig, paginationConf.pageSize]);
|
||||
|
||||
|
||||
const doSort = useCallback(() => {
|
||||
const sortedList = [...allConsumerGroupList];
|
||||
|
||||
if (sortConfig.sortKey === 'diffTotal') {
|
||||
sortedList.sort((a, b) => {
|
||||
return (a.diffTotal > b.diffTotal) ? sortConfig.sortOrder :
|
||||
((b.diffTotal > a.diffTotal) ? -sortConfig.sortOrder : 0);
|
||||
});
|
||||
}
|
||||
if (sortConfig.sortKey === 'group') {
|
||||
sortedList.sort((a, b) => {
|
||||
return (a.group > b.group) ? sortConfig.sortOrder :
|
||||
((b.group > a.group) ? -sortConfig.sortOrder : 0);
|
||||
});
|
||||
}
|
||||
if (sortConfig.sortKey === 'count') {
|
||||
sortedList.sort((a, b) => {
|
||||
return (a.count > b.count) ? sortConfig.sortOrder :
|
||||
((b.count > a.count) ? -sortConfig.sortOrder : 0);
|
||||
});
|
||||
}
|
||||
if (sortConfig.sortKey === 'consumeTps') {
|
||||
sortedList.sort((a, b) => {
|
||||
return (a.consumeTps > b.consumeTps) ? sortConfig.sortOrder :
|
||||
((b.consumeTps > a.consumeTps) ? -sortConfig.sortOrder : 0);
|
||||
});
|
||||
}
|
||||
|
||||
setAllConsumerGroupList(sortedList);
|
||||
filterList(paginationConf.current, sortedList);
|
||||
}, [sortConfig, allConsumerGroupList, paginationConf.current]);
|
||||
|
||||
useEffect(() => {
|
||||
loadConsumerGroups();
|
||||
}, [loadConsumerGroups]);
|
||||
|
||||
useEffect(() => {
|
||||
let intervalId;
|
||||
if (intervalProcessSwitch) {
|
||||
intervalId = setInterval(loadConsumerGroups, 10000);
|
||||
}
|
||||
return () => clearInterval(intervalId);
|
||||
}, [intervalProcessSwitch, loadConsumerGroups]);
|
||||
|
||||
|
||||
useEffect(() => {
|
||||
filterList(paginationConf.current, allConsumerGroupList);
|
||||
}, [allConsumerGroupList, filterStr, filterNormal, filterSystem, filterFIFO, sortConfig, filterList, paginationConf.current]);
|
||||
|
||||
const handleFilterInputChange = (value) => {
|
||||
setFilterStr(value);
|
||||
setPaginationConf(prev => ({...prev, current: 1}));
|
||||
};
|
||||
|
||||
const handleTypeFilterChange = (filterType, checked) => {
|
||||
switch (filterType) {
|
||||
case 'normal':
|
||||
setFilterNormal(checked);
|
||||
break;
|
||||
case 'fifo':
|
||||
setFilterFIFO(checked);
|
||||
break;
|
||||
case 'system':
|
||||
setFilterSystem(checked);
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
setPaginationConf(prev => ({...prev, current: 1}));
|
||||
};
|
||||
|
||||
const handleRefreshConsumerData = async () => {
|
||||
setLoading(true);
|
||||
const refreshResult = await remoteApi.refreshAllConsumerGroup();
|
||||
setLoading(false);
|
||||
|
||||
if (refreshResult && refreshResult.status === 0) {
|
||||
notificationApi.success({message: t.REFRESH_SUCCESS, duration: 2});
|
||||
loadConsumerGroups();
|
||||
} else if (refreshResult && refreshResult.errMsg) {
|
||||
notificationApi.error({message: t.REFRESH_FAILED + ": " + refreshResult.errMsg, duration: 2});
|
||||
} else {
|
||||
notificationApi.error({message: t.REFRESH_FAILED, duration: 2});
|
||||
}
|
||||
};
|
||||
|
||||
const handleOpenAddDialog = () => {
|
||||
setIsAddConfig(true)
|
||||
setShowConfig(true);
|
||||
};
|
||||
|
||||
// 修改操作按钮的点击处理函数
|
||||
const handleClient = (group, address) => {
|
||||
setSelectedGroup(group);
|
||||
setSelectedAddress(address);
|
||||
setShowClientInfo(true);
|
||||
};
|
||||
|
||||
const handleDetail = (group, address) => {
|
||||
setSelectedGroup(group);
|
||||
setSelectedAddress(address);
|
||||
setShowConsumeDetail(true);
|
||||
};
|
||||
|
||||
const handleUpdateConfigDialog = (group) => {
|
||||
setSelectedGroup(group);
|
||||
setShowConfig(true);
|
||||
};
|
||||
|
||||
|
||||
const handleDelete = (group) => {
|
||||
setSelectedGroup(group);
|
||||
setShowDeleteModal(true);
|
||||
};
|
||||
|
||||
const handleRefreshConsumerGroup = async (group) => {
|
||||
setLoading(true);
|
||||
const response = await remoteApi.refreshConsumerGroup(group);
|
||||
setLoading(false);
|
||||
if (response.status === 0) {
|
||||
messageApi.success({content: `${group} ${t.REFRESHED}`});
|
||||
loadConsumerGroups(paginationConf.current);
|
||||
} else {
|
||||
messageApi.error({title: t.ERROR, content: response.errMsg});
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
const handleSort = (sortKey) => {
|
||||
setSortConfig(prev => ({
|
||||
sortKey,
|
||||
sortOrder: prev.sortKey === sortKey ? -prev.sortOrder : 1,
|
||||
}));
|
||||
setPaginationConf(prev => ({...prev, current: 1}));
|
||||
};
|
||||
|
||||
const columns = [
|
||||
{
|
||||
title: <a onClick={() => handleSort('group')}>{t.SUBSCRIPTION_GROUP}</a>,
|
||||
dataIndex: 'group',
|
||||
key: 'group',
|
||||
align: 'center',
|
||||
render: (text) => {
|
||||
const sysFlag = text.startsWith('%SYS%');
|
||||
return (
|
||||
<span style={{color: sysFlag ? 'red' : ''}}>
|
||||
{sysFlag ? text.substring(5) : text}
|
||||
</span>
|
||||
);
|
||||
},
|
||||
},
|
||||
{
|
||||
title: <a onClick={() => handleSort('count')}>{t.QUANTITY}</a>,
|
||||
dataIndex: 'count',
|
||||
key: 'count',
|
||||
align: 'center',
|
||||
},
|
||||
{
|
||||
title: t.VERSION,
|
||||
dataIndex: 'version',
|
||||
key: 'version',
|
||||
align: 'center',
|
||||
},
|
||||
{
|
||||
title: t.TYPE,
|
||||
dataIndex: 'consumeType',
|
||||
key: 'consumeType',
|
||||
align: 'center',
|
||||
},
|
||||
{
|
||||
title: t.MODE,
|
||||
dataIndex: 'messageModel',
|
||||
key: 'messageModel',
|
||||
align: 'center',
|
||||
},
|
||||
{
|
||||
title: <a onClick={() => handleSort('consumeTps')}>TPS</a>,
|
||||
dataIndex: 'consumeTps',
|
||||
key: 'consumeTps',
|
||||
align: 'center',
|
||||
},
|
||||
{
|
||||
title: <a onClick={() => handleSort('diffTotal')}>{t.DELAY}</a>,
|
||||
dataIndex: 'diffTotal',
|
||||
key: 'diffTotal',
|
||||
align: 'center',
|
||||
},
|
||||
{
|
||||
title: t.UPDATE_TIME,
|
||||
dataIndex: 'updateTime',
|
||||
key: 'updateTime',
|
||||
align: 'center',
|
||||
},
|
||||
{
|
||||
title: t.OPERATION,
|
||||
key: 'operation',
|
||||
align: 'left',
|
||||
render: (_, record) => {
|
||||
const sysFlag = record.group.startsWith('%SYS%');
|
||||
return (
|
||||
<>
|
||||
<Button
|
||||
type="primary"
|
||||
size="small"
|
||||
style={{marginRight: 8, marginBottom: 8}}
|
||||
onClick={() => handleClient(record.group, record.address)}
|
||||
>
|
||||
{t.CLIENT}
|
||||
</Button>
|
||||
<Button
|
||||
type="primary"
|
||||
size="small"
|
||||
style={{marginRight: 8, marginBottom: 8}}
|
||||
onClick={() => handleDetail(record.group, record.address)}
|
||||
>
|
||||
{t.CONSUME_DETAIL}
|
||||
</Button>
|
||||
<Button
|
||||
type="primary"
|
||||
size="small"
|
||||
style={{marginRight: 8, marginBottom: 8}}
|
||||
onClick={() => handleUpdateConfigDialog(record.group)}
|
||||
>
|
||||
{t.CONFIG}
|
||||
</Button>
|
||||
<Button
|
||||
type="primary"
|
||||
size="small"
|
||||
style={{marginRight: 8, marginBottom: 8}}
|
||||
onClick={() => handleRefreshConsumerGroup(record.group)}
|
||||
>
|
||||
{t.REFRESH}
|
||||
</Button>
|
||||
{!sysFlag && writeOperationEnabled && (
|
||||
<Button
|
||||
type="primary"
|
||||
danger
|
||||
size="small"
|
||||
style={{marginRight: 8, marginBottom: 8}}
|
||||
onClick={() => handleDelete(record.group)}
|
||||
>
|
||||
{t.DELETE}
|
||||
</Button>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
},
|
||||
},
|
||||
];
|
||||
|
||||
const handleTableChange = (pagination) => {
|
||||
setPaginationConf(prev => ({
|
||||
...prev,
|
||||
current: pagination.current,
|
||||
pageSize: pagination.pageSize
|
||||
}));
|
||||
filterList(pagination.current, allConsumerGroupList);
|
||||
};
|
||||
|
||||
const closeConfigModal = () =>{
|
||||
setShowConfig(false);
|
||||
setIsAddConfig(false);
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
{msgContextHolder}
|
||||
{notificationContextHolder}
|
||||
<div style={{padding: '20px'}}>
|
||||
<Spin spinning={loading} tip={t.LOADING}>
|
||||
<div style={{marginBottom: '20px'}}>
|
||||
<div style={{display: 'flex', alignItems: 'center', gap: '15px'}}>
|
||||
<div style={{display: 'flex', alignItems: 'center'}}>
|
||||
<label style={{marginRight: '8px'}}>{t.SUBSCRIPTION_GROUP}:</label>
|
||||
<Input
|
||||
style={{width: '200px'}}
|
||||
value={filterStr}
|
||||
onChange={(e) => handleFilterInputChange(e.target.value)}
|
||||
/>
|
||||
</div>
|
||||
<Checkbox checked={filterNormal}
|
||||
onChange={(e) => handleTypeFilterChange('normal', e.target.checked)}>
|
||||
{t.NORMAL}
|
||||
</Checkbox>
|
||||
{rmqVersion && (
|
||||
<Checkbox checked={filterFIFO}
|
||||
onChange={(e) => handleTypeFilterChange('fifo', e.target.checked)}>
|
||||
{t.FIFO}
|
||||
</Checkbox>
|
||||
)}
|
||||
<Checkbox checked={filterSystem}
|
||||
onChange={(e) => handleTypeFilterChange('system', e.target.checked)}>
|
||||
{t.SYSTEM}
|
||||
</Checkbox>
|
||||
{writeOperationEnabled && (
|
||||
<Button type="primary" onClick={handleOpenAddDialog}>
|
||||
{t.ADD} / {t.UPDATE}
|
||||
</Button>
|
||||
)}
|
||||
<Button type="primary" onClick={handleRefreshConsumerData}>
|
||||
{t.REFRESH}
|
||||
</Button>
|
||||
{/*<Switch*/}
|
||||
{/* checked={intervalProcessSwitch}*/}
|
||||
{/* onChange={(checked) => setIntervalProcessSwitch(checked)}*/}
|
||||
{/* checkedChildren={t.AUTO_REFRESH}*/}
|
||||
{/* unCheckedChildren={t.AUTO_REFRESH}*/}
|
||||
{/*/>*/}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<Table
|
||||
dataSource={consumerGroupShowList}
|
||||
columns={columns}
|
||||
rowKey="group"
|
||||
bordered
|
||||
pagination={paginationConf}
|
||||
onChange={handleTableChange}
|
||||
sortDirections={['ascend', 'descend']}
|
||||
/>
|
||||
</Spin>
|
||||
|
||||
<ClientInfoModal
|
||||
visible={showClientInfo}
|
||||
group={selectedGroup}
|
||||
address={selectedAddress}
|
||||
onCancel={() => setShowClientInfo(false)}
|
||||
/>
|
||||
|
||||
<ConsumerDetailModal
|
||||
visible={showConsumeDetail}
|
||||
group={selectedGroup}
|
||||
address={selectedAddress}
|
||||
onCancel={() => setShowConsumeDetail(false)}
|
||||
/>
|
||||
|
||||
<ConsumerConfigModal
|
||||
visible={showConfig}
|
||||
isAddConfig={isAddConfig}
|
||||
group={selectedGroup}
|
||||
onCancel={closeConfigModal}
|
||||
setIsAddConfig={setIsAddConfig}
|
||||
onSuccess={loadConsumerGroups}
|
||||
/>
|
||||
|
||||
<DeleteConsumerModal
|
||||
visible={showDeleteModal}
|
||||
group={selectedGroup}
|
||||
onCancel={() => setShowDeleteModal(false)}
|
||||
onSuccess={loadConsumerGroups}
|
||||
/>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default ConsumerGroupList;
|
455
frontend-new/src/pages/Dashboard/DashboardPage.jsx
Normal file
455
frontend-new/src/pages/Dashboard/DashboardPage.jsx
Normal file
@@ -0,0 +1,455 @@
|
||||
/*
|
||||
* Licensed to the Apache Software Foundation (ASF) under one or more
|
||||
* contributor license agreements. See the NOTICE file distributed with
|
||||
* this work for additional information regarding copyright ownership.
|
||||
* The ASF licenses this file to You under the Apache License, Version 2.0
|
||||
* (the "License"); you may not use this file except in compliance with
|
||||
* the License. You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import React, {useCallback, useEffect, useRef, useState} from 'react';
|
||||
import {Card, Col, DatePicker, message, notification, Row, Select, Spin, Table} from 'antd';
|
||||
import * as echarts from 'echarts';
|
||||
import moment from 'moment';
|
||||
import {useLanguage} from '../../i18n/LanguageContext';
|
||||
import {remoteApi, tools} from '../../api/remoteApi/remoteApi';
|
||||
|
||||
const {Option} = Select;
|
||||
|
||||
const DashboardPage = () => {
|
||||
const {t} = useLanguage();
|
||||
const barChartRef = useRef(null);
|
||||
const lineChartRef = useRef(null);
|
||||
const topicBarChartRef = useRef(null);
|
||||
const topicLineChartRef = useRef(null);
|
||||
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [date, setDate] = useState(moment());
|
||||
const [topicNames, setTopicNames] = useState([]);
|
||||
const [selectedTopic, setSelectedTopic] = useState(null);
|
||||
const [brokerTableData, setBrokerTableData] = useState([]);
|
||||
|
||||
|
||||
const barChartInstance = useRef(null);
|
||||
const lineChartInstance = useRef(null);
|
||||
const topicBarChartInstance = useRef(null);
|
||||
const topicLineChartInstance = useRef(null);
|
||||
|
||||
const [messageApi, msgContextHolder] = message.useMessage();
|
||||
const [notificationApi, notificationContextHolder] = notification.useNotification();
|
||||
|
||||
const initChart = useCallback((chartRef, titleText, isLine = false) => {
|
||||
if (chartRef.current) {
|
||||
const chart = echarts.init(chartRef.current);
|
||||
let option = {
|
||||
title: {text: titleText},
|
||||
tooltip: {},
|
||||
legend: {data: ['TotalMsg']},
|
||||
axisPointer: {type: 'shadow'},
|
||||
xAxis: {
|
||||
type: 'category',
|
||||
data: [],
|
||||
axisLabel: {
|
||||
inside: false,
|
||||
color: '#000000',
|
||||
rotate: 0,
|
||||
interval: 0
|
||||
},
|
||||
axisTick: {show: true},
|
||||
axisLine: {show: true},
|
||||
z: 10
|
||||
},
|
||||
yAxis: {
|
||||
type: 'value',
|
||||
boundaryGap: [0, '100%'],
|
||||
axisLabel: {formatter: (value) => value.toFixed(2)},
|
||||
splitLine: {show: true}
|
||||
},
|
||||
series: [{name: 'TotalMsg', type: 'bar', data: []}]
|
||||
};
|
||||
|
||||
if (isLine) {
|
||||
option = {
|
||||
title: {text: titleText},
|
||||
toolbox: {
|
||||
feature: {
|
||||
dataZoom: {yAxisIndex: 'none'},
|
||||
restore: {},
|
||||
saveAsImage: {}
|
||||
}
|
||||
},
|
||||
tooltip: {trigger: 'axis', axisPointer: {animation: false}},
|
||||
yAxis: {
|
||||
type: 'value',
|
||||
boundaryGap: [0, '80%'],
|
||||
axisLabel: {formatter: (value) => value.toFixed(2)},
|
||||
splitLine: {show: true}
|
||||
},
|
||||
dataZoom: [{
|
||||
type: 'inside', start: 90, end: 100
|
||||
}, {
|
||||
start: 0,
|
||||
end: 10,
|
||||
handleIcon: 'M10.7,11.9v-1.3H9.3v1.3c-4.9,0.3-8.8,4.4-8.8,9.4c0,5,3.9,9.1,8.8,9.4v1.3h1.3v-1.3c4.9-0.3,8.8-4.4,8.8-9.4C19.5,16.3,15.6,12.2,10.7,11.9z M13.3,24.4H6.7V23h6.6V24.4z M13.3,19.6H6.7v-1.4h6.6V19.6z',
|
||||
handleSize: '80%',
|
||||
handleStyle: {
|
||||
color: '#fff',
|
||||
shadowBlur: 3,
|
||||
shadowColor: 'rgba(0, 0, 0, 0.6)',
|
||||
shadowOffsetX: 2,
|
||||
shadowOffsetY: 2
|
||||
}
|
||||
}],
|
||||
legend: {data: [], top: 30},
|
||||
xAxis: {type: 'time', boundaryGap: false, data: []},
|
||||
series: []
|
||||
};
|
||||
}
|
||||
chart.setOption(option);
|
||||
return chart;
|
||||
}
|
||||
return null;
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
barChartInstance.current = initChart(barChartRef, t.BROKER + ' TOP 10');
|
||||
lineChartInstance.current = initChart(lineChartRef, t.BROKER + ' 5min trend', true);
|
||||
topicBarChartInstance.current = initChart(topicBarChartRef, t.TOPIC + ' TOP 10');
|
||||
topicLineChartInstance.current = initChart(topicLineChartRef, t.TOPIC + ' 5min trend', true);
|
||||
|
||||
return () => {
|
||||
barChartInstance.current?.dispose();
|
||||
lineChartInstance.current?.dispose();
|
||||
topicBarChartInstance.current?.dispose();
|
||||
topicLineChartInstance.current?.dispose();
|
||||
};
|
||||
}, [t, initChart]);
|
||||
|
||||
const getBrokerBarChartOp = useCallback((xAxisData, data) => {
|
||||
return {
|
||||
xAxis: {data: xAxisData},
|
||||
series: [{name: 'TotalMsg', data: data}]
|
||||
};
|
||||
}, []);
|
||||
|
||||
const getBrokerLineChartOp = useCallback((legend, data) => {
|
||||
const series = [];
|
||||
let xAxisData = [];
|
||||
let isFirstSeries = true;
|
||||
|
||||
Object.entries(data).forEach(([key, values]) => {
|
||||
const tpsValues = [];
|
||||
values.forEach(tpsValue => {
|
||||
const tpsArray = tpsValue.split(",");
|
||||
if (isFirstSeries) {
|
||||
xAxisData.push(moment(parseInt(tpsArray[0])).format("HH:mm:ss"));
|
||||
}
|
||||
tpsValues.push(parseFloat(tpsArray[1]));
|
||||
});
|
||||
isFirstSeries = false;
|
||||
series.push({
|
||||
name: key,
|
||||
type: 'line',
|
||||
smooth: true,
|
||||
symbol: 'none',
|
||||
sampling: 'average',
|
||||
data: tpsValues
|
||||
});
|
||||
});
|
||||
|
||||
return {
|
||||
legend: {data: legend},
|
||||
color: ["#FF0000", "#00BFFF", "#FF00FF", "#1ce322", "#000000", '#EE7942'],
|
||||
xAxis: {type: 'category', boundaryGap: false, data: xAxisData},
|
||||
series: series
|
||||
};
|
||||
}, []);
|
||||
|
||||
const getTopicLineChartOp = useCallback((legend, data) => {
|
||||
const series = [];
|
||||
let xAxisData = [];
|
||||
let isFirstSeries = true;
|
||||
|
||||
Object.entries(data).forEach(([key, values]) => {
|
||||
const tpsValues = [];
|
||||
values.forEach(tpsValue => {
|
||||
const tpsArray = tpsValue.split(",");
|
||||
if (isFirstSeries) {
|
||||
xAxisData.push(moment(parseInt(tpsArray[0])).format("HH:mm:ss"));
|
||||
}
|
||||
tpsValues.push(parseFloat(tpsArray[2]));
|
||||
});
|
||||
isFirstSeries = false;
|
||||
series.push({
|
||||
name: key,
|
||||
type: 'line',
|
||||
smooth: true,
|
||||
symbol: 'none',
|
||||
sampling: 'average',
|
||||
data: tpsValues
|
||||
});
|
||||
});
|
||||
|
||||
return {
|
||||
legend: {data: legend},
|
||||
xAxis: {type: 'category', boundaryGap: false, data: xAxisData},
|
||||
series: series
|
||||
};
|
||||
}, []);
|
||||
|
||||
const queryLineData = useCallback(async () => {
|
||||
const _date = date ? date.format("YYYY-MM-DD") : moment().format("YYYY-MM-DD");
|
||||
|
||||
lineChartInstance.current?.showLoading();
|
||||
await remoteApi.queryBrokerHisData(_date, (resp) => {
|
||||
lineChartInstance.current?.hideLoading();
|
||||
if (resp.status === 0) {
|
||||
const _data = {};
|
||||
const _xAxisData = [];
|
||||
Object.entries(resp.data).forEach(([address, values]) => {
|
||||
_data[address] = values;
|
||||
_xAxisData.push(address);
|
||||
});
|
||||
lineChartInstance.current?.setOption(getBrokerLineChartOp(_xAxisData, _data));
|
||||
} else {
|
||||
notificationApi.error({message: resp.errMsg || t.QUERY_BROKER_HISTORY_FAILED, duration: 2});
|
||||
}
|
||||
});
|
||||
|
||||
if (selectedTopic) {
|
||||
topicLineChartInstance.current?.showLoading();
|
||||
await remoteApi.queryTopicHisData(_date, selectedTopic, (resp) => {
|
||||
topicLineChartInstance.current?.hideLoading();
|
||||
if (resp.status === 0) {
|
||||
const _data = {};
|
||||
_data[selectedTopic] = resp.data;
|
||||
topicLineChartInstance.current?.setOption(getTopicLineChartOp([selectedTopic], _data));
|
||||
} else {
|
||||
notificationApi.error({message: resp.errMsg || t.QUERY_TOPIC_HISTORY_FAILED, duration: 2});
|
||||
}
|
||||
});
|
||||
}
|
||||
}, [date, selectedTopic, getBrokerLineChartOp, getTopicLineChartOp, t]);
|
||||
|
||||
useEffect(() => {
|
||||
setLoading(true);
|
||||
barChartInstance.current?.showLoading();
|
||||
remoteApi.queryClusterList((resp) => {
|
||||
setLoading(false);
|
||||
barChartInstance.current?.hideLoading();
|
||||
if (resp.status === 0) {
|
||||
const clusterAddrTable = resp.data.clusterInfo.clusterAddrTable;
|
||||
const brokerAddrTable = resp.data.clusterInfo.brokerAddrTable; // Corrected to brokerAddrTable
|
||||
const brokerDetail = resp.data.brokerServer;
|
||||
const clusterMap = tools.generateBrokerMap(brokerDetail, clusterAddrTable, brokerAddrTable);
|
||||
|
||||
let brokerArray = [];
|
||||
Object.values(clusterMap).forEach(brokersInCluster => {
|
||||
brokerArray = brokerArray.concat(brokersInCluster);
|
||||
});
|
||||
|
||||
// Update broker table data
|
||||
setBrokerTableData(brokerArray.map(broker => ({
|
||||
...broker,
|
||||
key: broker.brokerName // Ant Design Table needs a unique key
|
||||
})));
|
||||
|
||||
brokerArray.sort((firstBroker, lastBroker) => {
|
||||
const firstTotalMsg = parseFloat(firstBroker.msgGetTotalTodayNow || 0);
|
||||
const lastTotalMsg = parseFloat(lastBroker.msgGetTotalTodayNow || 0);
|
||||
return lastTotalMsg - firstTotalMsg;
|
||||
});
|
||||
|
||||
const xAxisData = [];
|
||||
const data = [];
|
||||
brokerArray.slice(0, 10).forEach(broker => {
|
||||
xAxisData.push(`${broker.brokerName}:${broker.index}`);
|
||||
data.push(parseFloat(broker.msgGetTotalTodayNow || 0));
|
||||
});
|
||||
barChartInstance.current?.setOption(getBrokerBarChartOp(xAxisData, data));
|
||||
} else {
|
||||
notificationApi.error({message: resp.errMsg || t.QUERY_CLUSTER_LIST_FAILED, duration: 2});
|
||||
}
|
||||
});
|
||||
}, [getBrokerBarChartOp, t]);
|
||||
|
||||
useEffect(() => {
|
||||
topicBarChartInstance.current?.showLoading();
|
||||
remoteApi.queryTopicCurrentData((resp) => {
|
||||
topicBarChartInstance.current?.hideLoading();
|
||||
if (resp.status === 0) {
|
||||
const topicList = resp.data;
|
||||
topicList.sort((first, last) => {
|
||||
const firstTotalMsg = parseFloat(first.split(",")[1] || 0);
|
||||
const lastTotalMsg = parseFloat(last.split(",")[1] || 0);
|
||||
return lastTotalMsg - firstTotalMsg;
|
||||
});
|
||||
|
||||
const xAxisData = [];
|
||||
const data = [];
|
||||
const names = [];
|
||||
|
||||
topicList.forEach((currentData) => {
|
||||
const currentArray = currentData.split(",");
|
||||
names.push(currentArray[0]);
|
||||
});
|
||||
setTopicNames(names);
|
||||
|
||||
if (names.length > 0 && selectedTopic === null) {
|
||||
setSelectedTopic(names[0]);
|
||||
}
|
||||
|
||||
topicList.slice(0, 10).forEach((currentData) => {
|
||||
const currentArray = currentData.split(",");
|
||||
xAxisData.push(currentArray[0]);
|
||||
data.push(parseFloat(currentArray[1] || 0));
|
||||
});
|
||||
|
||||
const option = {
|
||||
xAxis: {
|
||||
data: xAxisData,
|
||||
axisLabel: {
|
||||
inside: false,
|
||||
color: '#000000',
|
||||
rotate: 60,
|
||||
interval: 0
|
||||
},
|
||||
},
|
||||
series: [{name: 'TotalMsg', data: data}]
|
||||
};
|
||||
topicBarChartInstance.current?.setOption(option);
|
||||
} else {
|
||||
notificationApi.error({message: resp.errMsg || t.QUERY_TOPIC_CURRENT_FAILED, duration: 2});
|
||||
}
|
||||
});
|
||||
}, [selectedTopic, t]);
|
||||
|
||||
useEffect(() => {
|
||||
if (barChartInstance.current && lineChartInstance.current && topicBarChartInstance.current && topicLineChartInstance.current) {
|
||||
queryLineData();
|
||||
}
|
||||
}, [date, selectedTopic, queryLineData]);
|
||||
|
||||
useEffect(() => {
|
||||
const intervalId = setInterval(queryLineData, tools.dashboardRefreshTime);
|
||||
return () => {
|
||||
clearInterval(intervalId);
|
||||
};
|
||||
}, [queryLineData]);
|
||||
|
||||
const brokerColumns = [
|
||||
{title: t.BROKER_NAME, dataIndex: 'brokerName', key: 'brokerName'},
|
||||
{title: t.BROKER_ADDR, dataIndex: 'brokerAddress', key: 'brokerAddress'},
|
||||
{
|
||||
title: t.TOTAL_MSG_RECEIVED_TODAY,
|
||||
dataIndex: 'msgGetTotalTodayNow',
|
||||
key: 'msgGetTotalTodayNow',
|
||||
render: (text) => parseFloat(text || 0).toLocaleString(),
|
||||
sorter: (a, b) => parseFloat(a.msgGetTotalTodayNow || 0) - parseFloat(b.msgGetTotalTodayNow || 0),
|
||||
},
|
||||
{
|
||||
title: t.TODAY_PRO_COUNT,
|
||||
key: 'todayProCount',
|
||||
render: (_, record) => parseFloat(record.msgPutTotalTodayMorning || 0).toLocaleString(), // Assuming msgPutTotalTodayMorning is 'today pro count'
|
||||
},
|
||||
{
|
||||
title: t.YESTERDAY_PRO_COUNT,
|
||||
key: 'yesterdayProCount',
|
||||
// This calculation (today morning - yesterday morning) might not be correct for 'yesterday pro count'.
|
||||
// It depends on what msgPutTotalTodayMorning and msgPutTotalYesterdayMorning truly represent.
|
||||
// If they are cumulative totals up to morning, then the difference is not accurate for yesterday's count.
|
||||
// You might need a specific 'msgPutTotalYesterdayNow' from the backend.
|
||||
render: (_, record) => (parseFloat(record.msgPutTotalTodayMorning || 0) - parseFloat(record.msgPutTotalYesterdayMorning || 0)).toLocaleString(),
|
||||
},
|
||||
];
|
||||
|
||||
return (
|
||||
<>
|
||||
{msgContextHolder}
|
||||
{notificationContextHolder}
|
||||
<div style={{padding: '20px'}}>
|
||||
<Spin spinning={loading} tip={t.LOADING}>
|
||||
<Row gutter={[16, 16]} style={{marginBottom: '20px'}}>
|
||||
<Col span={12}>
|
||||
<Card title={t.BROKER_OVERVIEW} bordered>
|
||||
<Table
|
||||
columns={brokerColumns}
|
||||
dataSource={brokerTableData}
|
||||
rowKey="key"
|
||||
pagination={false}
|
||||
size="small"
|
||||
bordered
|
||||
scroll={{y: 240}}
|
||||
/>
|
||||
</Card>
|
||||
</Col>
|
||||
<Col span={12}>
|
||||
<Card title={t.DASHBOARD_DATE_SELECTION} bordered>
|
||||
<DatePicker
|
||||
format="YYYY-MM-DD"
|
||||
value={date}
|
||||
onChange={setDate}
|
||||
allowClear
|
||||
style={{width: '100%'}}
|
||||
/>
|
||||
</Card>
|
||||
</Col>
|
||||
</Row>
|
||||
|
||||
<Row gutter={[16, 16]} style={{marginBottom: '20px'}}>
|
||||
<Col span={12}>
|
||||
<Card title={`${t.BROKER} TOP 10`} bordered>
|
||||
<div ref={barChartRef} style={{height: 300}}/>
|
||||
</Card>
|
||||
</Col>
|
||||
<Col span={12}>
|
||||
<Card title={`${t.BROKER} 5min ${t.TREND}`} bordered>
|
||||
<div ref={lineChartRef} style={{height: 300}}/>
|
||||
</Card>
|
||||
</Col>
|
||||
</Row>
|
||||
|
||||
<Row gutter={[16, 16]}>
|
||||
<Col span={12}>
|
||||
<Card title={`${t.TOPIC} TOP 10`} bordered>
|
||||
<div ref={topicBarChartRef} style={{height: 300}}/>
|
||||
</Card>
|
||||
</Col>
|
||||
<Col span={12}>
|
||||
<Card title={`${t.TOPIC} 5min ${t.TREND}`} bordered>
|
||||
<div style={{marginBottom: '10px'}}>
|
||||
<Select
|
||||
showSearch
|
||||
style={{width: '100%'}}
|
||||
placeholder={t.SELECT_TOPIC_PLACEHOLDER}
|
||||
value={selectedTopic}
|
||||
onChange={setSelectedTopic}
|
||||
filterOption={(input, option) =>
|
||||
option.children.toLowerCase().indexOf(input.toLowerCase()) >= 0
|
||||
}
|
||||
>
|
||||
{topicNames.map(topic => (
|
||||
<Option key={topic} value={topic}>{topic}</Option>
|
||||
))}
|
||||
</Select>
|
||||
</div>
|
||||
<div ref={topicLineChartRef} style={{height: 300}}/>
|
||||
</Card>
|
||||
</Col>
|
||||
</Row>
|
||||
</Spin>
|
||||
</div>
|
||||
</>
|
||||
|
||||
);
|
||||
};
|
||||
|
||||
export default DashboardPage;
|
703
frontend-new/src/pages/DlqMessage/dlqmessage.jsx
Normal file
703
frontend-new/src/pages/DlqMessage/dlqmessage.jsx
Normal file
@@ -0,0 +1,703 @@
|
||||
/*
|
||||
* Licensed to the Apache Software Foundation (ASF) under one or more
|
||||
* contributor license agreements. See the NOTICE file distributed with
|
||||
* this work for additional information regarding copyright ownership.
|
||||
* The ASF licenses this file to You under the Apache License, Version 2.0
|
||||
* (the "License"); you may not use this file except in compliance with
|
||||
* the License. You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import React, {useCallback, useEffect, useState} from 'react';
|
||||
import {
|
||||
Button,
|
||||
Checkbox,
|
||||
DatePicker,
|
||||
Form,
|
||||
Input,
|
||||
Modal,
|
||||
notification,
|
||||
Select,
|
||||
Spin,
|
||||
Table,
|
||||
Tabs,
|
||||
Typography
|
||||
} from 'antd';
|
||||
import moment from 'moment';
|
||||
import {ExportOutlined, SearchOutlined, SendOutlined} from '@ant-design/icons';
|
||||
import DlqMessageDetailViewDialog from "../../components/DlqMessageDetailViewDialog"; // Ensure this path is correct
|
||||
import {useLanguage} from '../../i18n/LanguageContext'; // Ensure this path is correct
|
||||
import {remoteApi} from '../../api/remoteApi/remoteApi'; // Adjust the path to your remoteApi.js file
|
||||
|
||||
const {TabPane} = Tabs;
|
||||
const {Option} = Select;
|
||||
const {Text, Paragraph} = Typography;
|
||||
|
||||
const SYS_GROUP_TOPIC_PREFIX = "CID_RMQ_SYS_"; // Define this constant as in Angular
|
||||
const DLQ_GROUP_TOPIC_PREFIX = "%DLQ%"; // Define this constant
|
||||
|
||||
const DlqMessageQueryPage = () => {
|
||||
const {t} = useLanguage();
|
||||
const [activeTab, setActiveTab] = useState('consumer');
|
||||
const [form] = Form.useForm();
|
||||
const [loading, setLoading] = useState(false);
|
||||
|
||||
// Consumer 查询状态
|
||||
const [allConsumerGroupList, setAllConsumerGroupList] = useState([]);
|
||||
const [selectedConsumerGroup, setSelectedConsumerGroup] = useState(null);
|
||||
const [timepickerBegin, setTimepickerBegin] = useState(moment().subtract(3, 'hour')); // 默认三小时前
|
||||
const [timepickerEnd, setTimepickerEnd] = useState(moment());
|
||||
const [messageShowList, setMessageShowList] = useState([]);
|
||||
const [paginationConf, setPaginationConf] = useState({
|
||||
current: 1,
|
||||
pageSize: 20, // Adjusted to 20 as per Angular code
|
||||
total: 0,
|
||||
});
|
||||
const [checkedAll, setCheckedAll] = useState(false);
|
||||
const [selectedMessageIds, setSelectedMessageIds] = useState(new Set()); // Stores msgId for selected messages
|
||||
const [messageCheckedList, setMessageCheckedList] = useState([]); // Stores full message objects for checked items
|
||||
const [taskId, setTaskId] = useState("");
|
||||
|
||||
|
||||
// Message ID 查询状态
|
||||
const [messageId, setMessageId] = useState('');
|
||||
const [queryDlqMessageByMessageIdResult, setQueryDlqMessageByMessageIdResult] = useState([]);
|
||||
const [modalApi, modalContextHolder] = Modal.useModal();
|
||||
const [notificationApi, notificationContextHolder] = notification.useNotification();
|
||||
// Fetch consumer group list on component mount
|
||||
useEffect(() => {
|
||||
const fetchConsumerGroups = async () => {
|
||||
setLoading(true);
|
||||
const resp = await remoteApi.queryConsumerGroupList();
|
||||
if (resp.status === 0) {
|
||||
const filteredGroups = resp.data
|
||||
.filter(consumerGroup => !consumerGroup.group.startsWith(SYS_GROUP_TOPIC_PREFIX))
|
||||
.map(consumerGroup => consumerGroup.group)
|
||||
.sort();
|
||||
setAllConsumerGroupList(filteredGroups);
|
||||
} else {
|
||||
notificationApi.error({message: t.ERROR, description: resp.errMsg});
|
||||
}
|
||||
setLoading(false);
|
||||
};
|
||||
fetchConsumerGroups();
|
||||
}, [t]);
|
||||
|
||||
// Effect to manage batch buttons' disabled state
|
||||
useEffect(() => {
|
||||
const batchResendBtn = document.getElementById('batchResendBtn');
|
||||
const batchExportBtn = document.getElementById('batchExportBtn');
|
||||
if (selectedMessageIds.size > 0) {
|
||||
batchResendBtn?.classList.remove('disabled');
|
||||
batchExportBtn?.classList.remove('disabled');
|
||||
} else {
|
||||
batchResendBtn?.classList.add('disabled');
|
||||
batchExportBtn?.classList.add('disabled');
|
||||
}
|
||||
}, [selectedMessageIds]);
|
||||
|
||||
const onChangeQueryCondition = useCallback(() => {
|
||||
// console.log("查询条件改变");
|
||||
setTaskId(""); // Reset taskId when query conditions change
|
||||
setPaginationConf(prev => ({...prev, currentPage: 1, totalItems: 0}));
|
||||
}, []);
|
||||
|
||||
const queryDlqMessageByConsumerGroup = useCallback(async (page = paginationConf.current, pageSize = paginationConf.pageSize) => {
|
||||
if (!selectedConsumerGroup) {
|
||||
notificationApi.warning({
|
||||
message: t.WARNING,
|
||||
description: t.PLEASE_SELECT_CONSUMER_GROUP,
|
||||
});
|
||||
return;
|
||||
}
|
||||
if (moment(timepickerEnd).valueOf() < moment(timepickerBegin).valueOf()) {
|
||||
notificationApi.error({message: t.END_TIME_LATER_THAN_BEGIN_TIME, delay: 2000});
|
||||
return;
|
||||
}
|
||||
|
||||
setLoading(true);
|
||||
// console.log("根据消费者组查询DLQ消息:", { selectedConsumerGroup, timepickerBegin, timepickerEnd, page, pageSize, taskId });
|
||||
try {
|
||||
const resp = await remoteApi.queryDlqMessageByConsumerGroup(
|
||||
selectedConsumerGroup,
|
||||
moment(timepickerBegin).valueOf(),
|
||||
moment(timepickerEnd).valueOf(),
|
||||
page,
|
||||
pageSize,
|
||||
taskId
|
||||
);
|
||||
|
||||
if (resp.status === 0) {
|
||||
const fetchedMessages = resp.data.page.content.map(msg => ({...msg, checked: false}));
|
||||
setMessageShowList(fetchedMessages);
|
||||
if (fetchedMessages.length === 0) {
|
||||
notificationApi.info({
|
||||
message: t.NO_RESULT,
|
||||
description: t.NO_MATCH_RESULT,
|
||||
});
|
||||
}
|
||||
setPaginationConf(prev => ({
|
||||
...prev,
|
||||
current: resp.data.page.number + 1,
|
||||
pageSize: pageSize,
|
||||
total: resp.data.page.totalElements,
|
||||
}));
|
||||
setTaskId(resp.data.taskId);
|
||||
setSelectedMessageIds(new Set()); // Reset选中项
|
||||
setCheckedAll(false); // Reset全选状态
|
||||
setMessageCheckedList([]); // Clear checked list
|
||||
} else {
|
||||
notificationApi.error({
|
||||
message: t.ERROR,
|
||||
description: resp.errMsg,
|
||||
});
|
||||
}
|
||||
} catch (error) {
|
||||
notificationApi.error({
|
||||
message: t.ERROR,
|
||||
description: t.QUERY_FAILED,
|
||||
});
|
||||
console.error("查询失败:", error);
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
}, [selectedConsumerGroup, timepickerBegin, timepickerEnd, paginationConf.current, paginationConf.pageSize, taskId, t]);
|
||||
|
||||
const queryDlqMessageByMessageId = useCallback(async () => {
|
||||
if (!messageId || !selectedConsumerGroup) {
|
||||
notificationApi.warning({
|
||||
message: t.WARNING,
|
||||
description: t.MESSAGE_ID_AND_CONSUMER_GROUP_REQUIRED,
|
||||
});
|
||||
return;
|
||||
}
|
||||
setLoading(true);
|
||||
// console.log("根据Message ID查询DLQ消息:", { msgId: messageId, consumerGroup: selectedConsumerGroup });
|
||||
try {
|
||||
const resp = await remoteApi.viewMessage(messageId, DLQ_GROUP_TOPIC_PREFIX + selectedConsumerGroup);
|
||||
if (resp.status === 0) {
|
||||
setQueryDlqMessageByMessageIdResult(resp.data ? [resp.data] : []);
|
||||
if (!resp.data) {
|
||||
notificationApi.info({
|
||||
message: t.NO_RESULT,
|
||||
description: t.NO_MATCH_RESULT,
|
||||
});
|
||||
}
|
||||
} else {
|
||||
notificationApi.error({
|
||||
message: t.ERROR,
|
||||
description: resp.errMsg,
|
||||
});
|
||||
}
|
||||
} catch (error) {
|
||||
notificationApi.error({
|
||||
message: t.ERROR,
|
||||
description: t.QUERY_FAILED,
|
||||
});
|
||||
console.error("查询失败:", error);
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
}, [messageId, selectedConsumerGroup, t]);
|
||||
|
||||
const queryDlqMessageDetail = useCallback(async (msgId, consumerGroup) => {
|
||||
setLoading(true);
|
||||
// console.log(`查询DLQ消息详情: ${msgId}, 消费者组: ${consumerGroup}`);
|
||||
try {
|
||||
const resp = await remoteApi.viewMessage(msgId, DLQ_GROUP_TOPIC_PREFIX + consumerGroup);
|
||||
if (resp.status === 0) {
|
||||
modalApi.info({
|
||||
title: t.MESSAGE_DETAIL,
|
||||
width: 800,
|
||||
content: (
|
||||
<DlqMessageDetailViewDialog
|
||||
ngDialogData={{messageView: resp.data}}
|
||||
/>
|
||||
),
|
||||
onOk: () => {
|
||||
},
|
||||
okText: t.CLOSE,
|
||||
});
|
||||
} else {
|
||||
notificationApi.error({
|
||||
message: t.ERROR,
|
||||
description: resp.errMsg,
|
||||
});
|
||||
}
|
||||
} catch (error) {
|
||||
notificationApi.error({
|
||||
message: t.ERROR,
|
||||
description: t.QUERY_FAILED,
|
||||
});
|
||||
console.error("查询失败:", error);
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
}, [t]);
|
||||
|
||||
const resendDlqMessage = useCallback(async (messageView, consumerGroup) => {
|
||||
setLoading(true);
|
||||
const topic = messageView.properties.RETRY_TOPIC;
|
||||
const msgId = messageView.properties.ORIGIN_MESSAGE_ID;
|
||||
// console.log(`重发DLQ消息: MsgId=${msgId}, Topic=${topic}, 消费者组=${consumerGroup}`);
|
||||
try {
|
||||
const resp = await remoteApi.resendDlqMessage(msgId, consumerGroup, topic);
|
||||
if (resp.status === 0) {
|
||||
notificationApi.success({
|
||||
message: t.SUCCESS,
|
||||
description: t.RESEND_SUCCESS,
|
||||
});
|
||||
modalApi.info({
|
||||
title: t.RESULT,
|
||||
content: resp.data,
|
||||
});
|
||||
// Refresh list
|
||||
queryDlqMessageByConsumerGroup(paginationConf.current, paginationConf.pageSize);
|
||||
} else {
|
||||
notificationApi.error({
|
||||
message: t.ERROR,
|
||||
description: resp.errMsg,
|
||||
});
|
||||
modalApi.error({
|
||||
title: t.RESULT,
|
||||
content: resp.errMsg,
|
||||
});
|
||||
}
|
||||
} catch (error) {
|
||||
notificationApi.error({
|
||||
message: t.ERROR,
|
||||
description: t.RESEND_FAILED,
|
||||
});
|
||||
console.error("重发失败:", error);
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
}, [paginationConf.current, paginationConf.pageSize, queryDlqMessageByConsumerGroup, t]);
|
||||
|
||||
const exportDlqMessage = useCallback(async (msgId, consumerGroup) => {
|
||||
setLoading(true);
|
||||
// console.log(`导出DLQ消息: MsgId=${msgId}, 消费者组=${consumerGroup}`);
|
||||
try {
|
||||
const resp = await remoteApi.exportDlqMessage(msgId, consumerGroup);
|
||||
if (resp.status === 0) {
|
||||
notificationApi.success({
|
||||
message: t.SUCCESS,
|
||||
description: t.EXPORT_SUCCESS,
|
||||
});
|
||||
// The actual file download is handled within remoteApi.js
|
||||
} else {
|
||||
notificationApi.error({
|
||||
message: t.ERROR,
|
||||
description: resp.errMsg,
|
||||
});
|
||||
}
|
||||
} catch (error) {
|
||||
notificationApi.error({
|
||||
message: t.ERROR,
|
||||
description: t.EXPORT_FAILED,
|
||||
});
|
||||
console.error("导出失败:", error);
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
}, [t]);
|
||||
|
||||
const batchResendDlqMessage = useCallback(async () => {
|
||||
if (selectedMessageIds.size === 0) {
|
||||
notificationApi.warning({
|
||||
message: t.WARNING,
|
||||
description: t.PLEASE_SELECT_MESSAGE_TO_RESEND,
|
||||
});
|
||||
return;
|
||||
}
|
||||
setLoading(true);
|
||||
const messagesToResend = messageCheckedList.map(message => ({
|
||||
topic: message.properties.RETRY_TOPIC,
|
||||
msgId: message.properties.ORIGIN_MESSAGE_ID,
|
||||
consumerGroup: selectedConsumerGroup,
|
||||
}));
|
||||
// console.log(`批量重发DLQ消息到 ${selectedConsumerGroup}:`, messagesToResend);
|
||||
try {
|
||||
const resp = await remoteApi.batchResendDlqMessage(messagesToResend);
|
||||
if (resp.status === 0) {
|
||||
notificationApi.success({
|
||||
message: t.SUCCESS,
|
||||
description: t.BATCH_RESEND_SUCCESS,
|
||||
});
|
||||
modalApi.info({
|
||||
title: t.RESULT,
|
||||
content: resp.data,
|
||||
});
|
||||
// Refresh list and reset selected state
|
||||
queryDlqMessageByConsumerGroup(paginationConf.current, paginationConf.pageSize);
|
||||
setSelectedMessageIds(new Set());
|
||||
setCheckedAll(false);
|
||||
setMessageCheckedList([]);
|
||||
} else {
|
||||
notificationApi.error({
|
||||
message: t.ERROR,
|
||||
description: resp.errMsg,
|
||||
});
|
||||
modalApi.error({
|
||||
title: t.RESULT,
|
||||
content: resp.errMsg,
|
||||
});
|
||||
}
|
||||
} catch (error) {
|
||||
notificationApi.error({
|
||||
message: t.ERROR,
|
||||
description: t.BATCH_RESEND_FAILED,
|
||||
});
|
||||
console.error("批量重发失败:", error);
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
}, [selectedMessageIds, messageCheckedList, selectedConsumerGroup, paginationConf.current, paginationConf.pageSize, queryDlqMessageByConsumerGroup, t]);
|
||||
|
||||
const batchExportDlqMessage = useCallback(async () => {
|
||||
if (selectedMessageIds.size === 0) {
|
||||
notificationApi.warning({
|
||||
message: t.WARNING,
|
||||
description: t.PLEASE_SELECT_MESSAGE_TO_EXPORT,
|
||||
});
|
||||
return;
|
||||
}
|
||||
setLoading(true);
|
||||
const messagesToExport = messageCheckedList.map(message => ({
|
||||
msgId: message.msgId,
|
||||
consumerGroup: selectedConsumerGroup,
|
||||
}));
|
||||
// console.log(`批量导出DLQ消息从 ${selectedConsumerGroup}:`, messagesToExport);
|
||||
try {
|
||||
const resp = await remoteApi.batchExportDlqMessage(messagesToExport);
|
||||
if (resp.status === 0) {
|
||||
notificationApi.success({
|
||||
message: t.SUCCESS,
|
||||
description: t.BATCH_EXPORT_SUCCESS,
|
||||
});
|
||||
// The actual file download is handled within remoteApi.js
|
||||
// Refresh list and reset selected state
|
||||
queryDlqMessageByConsumerGroup(paginationConf.current, paginationConf.pageSize);
|
||||
setSelectedMessageIds(new Set());
|
||||
setCheckedAll(false);
|
||||
setMessageCheckedList([]);
|
||||
} else {
|
||||
notificationApi.error({
|
||||
message: t.ERROR,
|
||||
description: resp.errMsg,
|
||||
});
|
||||
}
|
||||
} catch (error) {
|
||||
notificationApi.error({
|
||||
message: t.ERROR,
|
||||
description: t.BATCH_EXPORT_FAILED,
|
||||
});
|
||||
console.error("批量导出失败:", error);
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
}, [selectedMessageIds, messageCheckedList, selectedConsumerGroup, paginationConf.current, paginationConf.pageSize, queryDlqMessageByConsumerGroup, t]);
|
||||
|
||||
const handleSelectAll = (e) => {
|
||||
const checked = e.target.checked;
|
||||
setCheckedAll(checked);
|
||||
const newSelectedIds = new Set();
|
||||
const newCheckedList = [];
|
||||
const updatedList = messageShowList.map(item => {
|
||||
if (checked) {
|
||||
newSelectedIds.add(item.msgId);
|
||||
newCheckedList.push(item);
|
||||
}
|
||||
return {...item, checked};
|
||||
});
|
||||
setMessageShowList(updatedList);
|
||||
setSelectedMessageIds(newSelectedIds);
|
||||
setMessageCheckedList(newCheckedList);
|
||||
};
|
||||
|
||||
const handleSelectItem = (item, checked) => {
|
||||
const newSelectedIds = new Set(selectedMessageIds);
|
||||
const newCheckedList = [...messageCheckedList];
|
||||
|
||||
if (checked) {
|
||||
newSelectedIds.add(item.msgId);
|
||||
newCheckedList.push(item);
|
||||
} else {
|
||||
newSelectedIds.delete(item.msgId);
|
||||
const index = newCheckedList.findIndex(msg => msg.msgId === item.msgId);
|
||||
if (index > -1) {
|
||||
newCheckedList.splice(index, 1);
|
||||
}
|
||||
}
|
||||
setSelectedMessageIds(newSelectedIds);
|
||||
setMessageCheckedList(newCheckedList);
|
||||
|
||||
// Update single item checked state in the displayed list
|
||||
const updatedList = messageShowList.map(msg =>
|
||||
msg.msgId === item.msgId ? {...msg, checked} : msg
|
||||
);
|
||||
setMessageShowList(updatedList);
|
||||
|
||||
// Check if all are selected
|
||||
setCheckedAll(newSelectedIds.size === updatedList.length && updatedList.length > 0);
|
||||
};
|
||||
|
||||
|
||||
const consumerColumns = [
|
||||
{
|
||||
title: (
|
||||
<Checkbox
|
||||
checked={checkedAll}
|
||||
onChange={handleSelectAll}
|
||||
disabled={messageShowList.length === 0}
|
||||
/>
|
||||
),
|
||||
dataIndex: 'checked',
|
||||
key: 'checkbox',
|
||||
align: 'center',
|
||||
render: (checked, record) => (
|
||||
<Checkbox
|
||||
checked={checked}
|
||||
onChange={(e) => handleSelectItem(record, e.target.checked)}
|
||||
/>
|
||||
),
|
||||
},
|
||||
{title: 'Message ID', dataIndex: 'msgId', key: 'msgId', align: 'center'},
|
||||
{
|
||||
title: 'Tag', dataIndex: ['properties', 'TAGS'], key: 'tags', align: 'center',
|
||||
render: (tags) => tags || '-' // Display '-' if tags are null or undefined
|
||||
},
|
||||
{
|
||||
title: 'Key', dataIndex: ['properties', 'KEYS'], key: 'keys', align: 'center',
|
||||
render: (keys) => keys || '-' // Display '-' if keys are null or undefined
|
||||
},
|
||||
{
|
||||
title: 'StoreTime',
|
||||
dataIndex: 'storeTimestamp',
|
||||
key: 'storeTimestamp',
|
||||
align: 'center',
|
||||
render: (text) => moment(text).format("YYYY-MM-DD HH:mm:ss"),
|
||||
},
|
||||
{
|
||||
title: 'Operation',
|
||||
key: 'operation',
|
||||
align: 'center',
|
||||
render: (_, record) => (
|
||||
<>
|
||||
<Button type="primary" size="small" style={{marginRight: 8, marginBottom: 8}}
|
||||
onClick={() => queryDlqMessageDetail(record.msgId, selectedConsumerGroup)}>
|
||||
{t.MESSAGE_DETAIL}
|
||||
</Button>
|
||||
<Button type="primary" size="small" style={{marginRight: 8, marginBottom: 8}}
|
||||
onClick={() => resendDlqMessage(record, selectedConsumerGroup)}>
|
||||
{t.RESEND_MESSAGE}
|
||||
</Button>
|
||||
<Button type="primary" size="small" style={{marginBottom: 8}}
|
||||
onClick={() => exportDlqMessage(record.msgId, selectedConsumerGroup)}>
|
||||
{t.EXPORT}
|
||||
</Button>
|
||||
</>
|
||||
),
|
||||
},
|
||||
];
|
||||
|
||||
const messageIdColumns = [
|
||||
{title: 'Message ID', dataIndex: 'msgId', key: 'msgId', align: 'center'},
|
||||
{
|
||||
title: 'Tag', dataIndex: ['properties', 'TAGS'], key: 'tags', align: 'center',
|
||||
render: (tags) => tags || '-'
|
||||
},
|
||||
{
|
||||
title: 'Key', dataIndex: ['properties', 'KEYS'], key: 'keys', align: 'center',
|
||||
render: (keys) => keys || '-'
|
||||
},
|
||||
{
|
||||
title: 'StoreTime',
|
||||
dataIndex: 'storeTimestamp',
|
||||
key: 'storeTimestamp',
|
||||
align: 'center',
|
||||
render: (text) => moment(text).format("YYYY-MM-DD HH:mm:ss"),
|
||||
},
|
||||
{
|
||||
title: 'Operation',
|
||||
key: 'operation',
|
||||
align: 'center',
|
||||
render: (_, record) => (
|
||||
<>
|
||||
<Button type="primary" size="small" style={{marginRight: 8, marginBottom: 8}}
|
||||
onClick={() => queryDlqMessageDetail(record.msgId, selectedConsumerGroup)}>
|
||||
{t.MESSAGE_DETAIL}
|
||||
</Button>
|
||||
<Button type="primary" size="small" style={{marginRight: 8, marginBottom: 8}}
|
||||
onClick={() => resendDlqMessage(record, selectedConsumerGroup)}>
|
||||
{t.RESEND_MESSAGE}
|
||||
</Button>
|
||||
<Button type="primary" size="small" style={{marginBottom: 8}}
|
||||
onClick={() => exportDlqMessage(record.msgId, selectedConsumerGroup)}>
|
||||
{t.EXPORT}
|
||||
</Button>
|
||||
</>
|
||||
),
|
||||
},
|
||||
];
|
||||
|
||||
return (
|
||||
<>
|
||||
{notificationContextHolder}
|
||||
<div style={{padding: '20px'}}>
|
||||
<Spin spinning={loading} tip="加载中...">
|
||||
<Tabs activeKey={activeTab} onChange={setActiveTab} centered>
|
||||
<TabPane tab={t.CONSUMER} key="consumer">
|
||||
<h5 style={{margin: '15px 0'}}>{t.TOTAL_MESSAGES}</h5>
|
||||
<div style={{padding: '20px', minHeight: '600px'}}>
|
||||
<Form layout="inline" form={form} style={{marginBottom: '20px'}}>
|
||||
<Form.Item label={t.CONSUMER}>
|
||||
<Select
|
||||
showSearch
|
||||
style={{width: 300}}
|
||||
placeholder={t.SELECT_CONSUMER_GROUP_PLACEHOLDER}
|
||||
value={selectedConsumerGroup}
|
||||
onChange={(value) => {
|
||||
setSelectedConsumerGroup(value);
|
||||
onChangeQueryCondition();
|
||||
}}
|
||||
filterOption={(input, option) =>
|
||||
option.children.toLowerCase().indexOf(input.toLowerCase()) >= 0
|
||||
}
|
||||
>
|
||||
{allConsumerGroupList.map(group => (
|
||||
<Option key={group} value={group}>{group}</Option>
|
||||
))}
|
||||
</Select>
|
||||
</Form.Item>
|
||||
<Form.Item label={t.BEGIN}>
|
||||
<DatePicker
|
||||
showTime
|
||||
format="YYYY-MM-DD HH:mm:ss"
|
||||
value={timepickerBegin}
|
||||
onChange={(date) => {
|
||||
setTimepickerBegin(date);
|
||||
onChangeQueryCondition();
|
||||
}}
|
||||
/>
|
||||
</Form.Item>
|
||||
<Form.Item label={t.END}>
|
||||
<DatePicker
|
||||
showTime
|
||||
format="YYYY-MM-DD HH:mm:ss"
|
||||
value={timepickerEnd}
|
||||
onChange={(date) => {
|
||||
setTimepickerEnd(date);
|
||||
onChangeQueryCondition();
|
||||
}}
|
||||
/>
|
||||
</Form.Item>
|
||||
<Form.Item>
|
||||
<Button type="primary" icon={<SearchOutlined/>}
|
||||
onClick={() => queryDlqMessageByConsumerGroup()}>
|
||||
{t.SEARCH}
|
||||
</Button>
|
||||
</Form.Item>
|
||||
<Form.Item>
|
||||
<Button
|
||||
id="batchResendBtn"
|
||||
type="primary"
|
||||
icon={<SendOutlined/>}
|
||||
onClick={batchResendDlqMessage}
|
||||
disabled={selectedMessageIds.size === 0}
|
||||
>
|
||||
{t.BATCH_RESEND}
|
||||
</Button>
|
||||
</Form.Item>
|
||||
<Form.Item>
|
||||
<Button
|
||||
id="batchExportBtn"
|
||||
type="primary"
|
||||
icon={<ExportOutlined/>}
|
||||
onClick={batchExportDlqMessage}
|
||||
disabled={selectedMessageIds.size === 0}
|
||||
>
|
||||
{t.BATCH_EXPORT}
|
||||
</Button>
|
||||
</Form.Item>
|
||||
</Form>
|
||||
<Table
|
||||
columns={consumerColumns}
|
||||
dataSource={messageShowList}
|
||||
rowKey="msgId"
|
||||
bordered
|
||||
pagination={{
|
||||
current: paginationConf.current,
|
||||
pageSize: paginationConf.pageSize,
|
||||
total: paginationConf.total,
|
||||
onChange: (page, pageSize) => queryDlqMessageByConsumerGroup(page, pageSize),
|
||||
showSizeChanger: true, // Allow changing page size
|
||||
pageSizeOptions: ['10', '20', '50', '100'], // Customizable page size options
|
||||
}}
|
||||
locale={{emptyText: t.NO_MATCH_RESULT}}
|
||||
/>
|
||||
</div>
|
||||
</TabPane>
|
||||
<TabPane tab="Message ID" key="messageId">
|
||||
<h5 style={{margin: '15px 0'}}>
|
||||
{t.MESSAGE_ID_CONSUMER_GROUP_HINT}
|
||||
</h5>
|
||||
<div style={{padding: '20px', minHeight: '600px'}}>
|
||||
<Form layout="inline" style={{marginBottom: '20px'}}>
|
||||
<Form.Item label={t.CONSUMER}>
|
||||
<Select
|
||||
showSearch
|
||||
style={{width: 300}}
|
||||
placeholder={t.SELECT_CONSUMER_GROUP_PLACEHOLDER}
|
||||
value={selectedConsumerGroup}
|
||||
onChange={setSelectedConsumerGroup}
|
||||
filterOption={(input, option) =>
|
||||
option.children.toLowerCase().indexOf(input.toLowerCase()) >= 0
|
||||
}
|
||||
>
|
||||
{allConsumerGroupList.map(group => (
|
||||
<Option key={group} value={group}>{group}</Option>
|
||||
))}
|
||||
</Select>
|
||||
</Form.Item>
|
||||
<Form.Item label="MessageId:">
|
||||
<Input
|
||||
style={{width: 450}}
|
||||
value={messageId}
|
||||
onChange={(e) => setMessageId(e.target.value)}
|
||||
/>
|
||||
</Form.Item>
|
||||
<Form.Item>
|
||||
<Button type="primary" icon={<SearchOutlined/>}
|
||||
onClick={queryDlqMessageByMessageId}>
|
||||
{t.SEARCH}
|
||||
</Button>
|
||||
</Form.Item>
|
||||
</Form>
|
||||
<Table
|
||||
columns={messageIdColumns}
|
||||
dataSource={queryDlqMessageByMessageIdResult}
|
||||
rowKey="msgId"
|
||||
bordered
|
||||
pagination={false}
|
||||
locale={{emptyText: t.NO_MATCH_RESULT}}
|
||||
/>
|
||||
</div>
|
||||
</TabPane>
|
||||
{modalContextHolder}
|
||||
</Tabs>
|
||||
</Spin>
|
||||
</div>
|
||||
</>
|
||||
|
||||
);
|
||||
};
|
||||
|
||||
export default DlqMessageQueryPage;
|
90
frontend-new/src/pages/Login/login.jsx
Normal file
90
frontend-new/src/pages/Login/login.jsx
Normal file
@@ -0,0 +1,90 @@
|
||||
/*
|
||||
* Licensed to the Apache Software Foundation (ASF) under one or more
|
||||
* contributor license agreements. See the NOTICE file distributed with
|
||||
* this work for additional information regarding copyright ownership.
|
||||
* The ASF licenses this file to You under the Apache License, Version 2.0
|
||||
* (the "License"); you may not use this file except in compliance with
|
||||
* the License. You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import React from 'react';
|
||||
import { Form, Input, Button, message, Typography } from 'antd';
|
||||
import {remoteApi} from "../../api/remoteApi/remoteApi";
|
||||
|
||||
const { Title } = Typography;
|
||||
|
||||
const Login = () => {
|
||||
const [form] = Form.useForm();
|
||||
const [messageApi, msgContextHolder] = message.useMessage();
|
||||
|
||||
const onFinish = async (values) => {
|
||||
const { username, password } = values;
|
||||
remoteApi.login(username, password).then((res) => {
|
||||
if (res.status === 0) {
|
||||
messageApi.success('登录成功');
|
||||
window.localStorage.setItem("username", res.data.loginUserName);
|
||||
window.localStorage.setItem("userrole", res.data.loginUserRole);
|
||||
window.location.href = '/';
|
||||
} else {
|
||||
messageApi.error(res.message || '登录失败,请检查用户名和密码');
|
||||
}
|
||||
})
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
{msgContextHolder}
|
||||
<div style={{
|
||||
maxWidth: 400,
|
||||
margin: '100px auto',
|
||||
padding: 24,
|
||||
boxShadow: '0 2px 8px #f0f1f2',
|
||||
borderRadius: 8
|
||||
}}>
|
||||
<Title level={3} style={{textAlign: 'center', marginBottom: 24}}>
|
||||
WELCOME
|
||||
</Title>
|
||||
<Form
|
||||
form={form}
|
||||
name="login_form"
|
||||
layout="vertical"
|
||||
onFinish={onFinish}
|
||||
initialValues={{username: '', password: ''}}
|
||||
>
|
||||
<Form.Item
|
||||
label="用户名"
|
||||
name="username"
|
||||
rules={[{required: true, message: '请输入用户名'}]}
|
||||
>
|
||||
<Input placeholder="请输入用户名"/>
|
||||
</Form.Item>
|
||||
|
||||
<Form.Item
|
||||
label="密码"
|
||||
name="password"
|
||||
rules={[{required: true, message: '请输入密码'}]}
|
||||
>
|
||||
<Input.Password placeholder="请输入密码"/>
|
||||
</Form.Item>
|
||||
|
||||
<Form.Item>
|
||||
<Button type="primary" htmlType="submit" block>
|
||||
登录
|
||||
</Button>
|
||||
</Form.Item>
|
||||
</Form>
|
||||
</div>
|
||||
</>
|
||||
|
||||
);
|
||||
};
|
||||
|
||||
export default Login;
|
478
frontend-new/src/pages/Message/message.jsx
Normal file
478
frontend-new/src/pages/Message/message.jsx
Normal file
@@ -0,0 +1,478 @@
|
||||
/*
|
||||
* Licensed to the Apache Software Foundation (ASF) under one or more
|
||||
* contributor license agreements. See the NOTICE file distributed with
|
||||
* this work for additional information regarding copyright ownership.
|
||||
* The ASF licenses this file to You under the Apache License, Version 2.0
|
||||
* (the "License"); you may not use this file except in compliance with
|
||||
* the License. You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import React, {useCallback, useEffect, useState} from 'react';
|
||||
import {Button, DatePicker, Form, Input, notification, Select, Spin, Table, Tabs, Typography} from 'antd';
|
||||
import moment from 'moment';
|
||||
import {SearchOutlined} from '@ant-design/icons';
|
||||
import {useLanguage} from '../../i18n/LanguageContext';
|
||||
import MessageDetailViewDialog from "../../components/MessageDetailViewDialog"; // Keep this path
|
||||
import {remoteApi} from '../../api/remoteApi/remoteApi'; // Keep this path
|
||||
|
||||
const {TabPane} = Tabs;
|
||||
const {Option} = Select;
|
||||
const {Text, Paragraph} = Typography;
|
||||
|
||||
const MessageQueryPage = () => {
|
||||
const {t} = useLanguage();
|
||||
const [activeTab, setActiveTab] = useState('topic');
|
||||
const [form] = Form.useForm();
|
||||
const [loading, setLoading] = useState(false);
|
||||
|
||||
// Topic 查询状态
|
||||
const [allTopicList, setAllTopicList] = useState([]);
|
||||
const [selectedTopic, setSelectedTopic] = useState(null);
|
||||
const [timepickerBegin, setTimepickerBegin] = useState(moment().subtract(1, 'hour')); // 默认一小时前
|
||||
const [timepickerEnd, setTimepickerEnd] = useState(moment());
|
||||
const [messageShowList, setMessageShowList] = useState([]);
|
||||
const [paginationConf, setPaginationConf] = useState({
|
||||
current: 1,
|
||||
pageSize: 10,
|
||||
total: 0,
|
||||
});
|
||||
const [taskId, setTaskId] = useState("");
|
||||
|
||||
// Message Key 查询状态
|
||||
const [key, setKey] = useState('');
|
||||
const [queryMessageByTopicAndKeyResult, setQueryMessageByTopicAndKeyResult] = useState([]);
|
||||
|
||||
// Message ID 查询状态
|
||||
const [messageId, setMessageId] = useState('');
|
||||
|
||||
// State for Message Detail Dialog
|
||||
const [isMessageDetailModalVisible, setIsMessageDetailModalVisible] = useState(false);
|
||||
const [currentMessageIdForDetail, setCurrentMessageIdForDetail] = useState(null);
|
||||
const [currentTopicForDetail, setCurrentTopicForDetail] = useState(null);
|
||||
const [notificationApi, notificationContextHolder] = notification.useNotification();
|
||||
|
||||
const fetchAllTopics = useCallback(async () => {
|
||||
setLoading(true);
|
||||
try {
|
||||
const resp = await remoteApi.queryTopic(false);
|
||||
if (resp.status === 0) {
|
||||
setAllTopicList(resp.data.topicList.sort());
|
||||
} else {
|
||||
notificationApi.error({
|
||||
message: t.ERROR,
|
||||
description: resp.errMsg || t.FETCH_TOPIC_FAILED,
|
||||
});
|
||||
}
|
||||
} catch (error) {
|
||||
notificationApi.error({
|
||||
message: t.ERROR,
|
||||
description: t.FETCH_TOPIC_FAILED,
|
||||
});
|
||||
console.error("Error fetching topic list:", error);
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
}, [t]);
|
||||
|
||||
useEffect(() => {
|
||||
fetchAllTopics();
|
||||
}, [fetchAllTopics]);
|
||||
|
||||
const onChangeQueryCondition = () => {
|
||||
setTaskId("");
|
||||
setPaginationConf(prev => ({
|
||||
...prev,
|
||||
current: 1,
|
||||
total: 0,
|
||||
}));
|
||||
};
|
||||
|
||||
const queryMessagePageByTopic = async (page = paginationConf.current, pageSize = paginationConf.pageSize) => {
|
||||
if (!selectedTopic) {
|
||||
notificationApi.warning({
|
||||
message: t.WARNING,
|
||||
description: t.PLEASE_SELECT_TOPIC,
|
||||
});
|
||||
return;
|
||||
}
|
||||
if (timepickerEnd.valueOf() < timepickerBegin.valueOf()) {
|
||||
notificationApi.error({message: t.ERROR, description: t.END_TIME_EARLIER_THAN_BEGIN_TIME});
|
||||
return;
|
||||
}
|
||||
|
||||
setLoading(true);
|
||||
try {
|
||||
const resp = await remoteApi.queryMessagePageByTopic(
|
||||
selectedTopic,
|
||||
timepickerBegin.valueOf(),
|
||||
timepickerEnd.valueOf(),
|
||||
page,
|
||||
pageSize,
|
||||
taskId
|
||||
);
|
||||
|
||||
if (resp.status === 0) {
|
||||
setMessageShowList(resp.data.page.content);
|
||||
setPaginationConf(prev => ({
|
||||
...prev,
|
||||
current: resp.data.page.number + 1,
|
||||
total: resp.data.page.totalElements,
|
||||
pageSize: pageSize,
|
||||
}));
|
||||
setTaskId(resp.data.taskId);
|
||||
|
||||
if (resp.data.page.content.length === 0) {
|
||||
notificationApi.info({
|
||||
message: t.NO_RESULT,
|
||||
description: t.NO_MATCH_RESULT,
|
||||
});
|
||||
}
|
||||
} else {
|
||||
notificationApi.error({
|
||||
message: t.ERROR,
|
||||
description: resp.errMsg || t.QUERY_FAILED,
|
||||
});
|
||||
}
|
||||
} catch (error) {
|
||||
notificationApi.error({
|
||||
message: t.ERROR,
|
||||
description: t.QUERY_FAILED,
|
||||
});
|
||||
console.error("查询失败:", error);
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
const queryMessageByTopicAndKey = async () => {
|
||||
if (!selectedTopic || !key) {
|
||||
notificationApi.warning({
|
||||
message: t.WARNING,
|
||||
description: t.TOPIC_AND_KEY_REQUIRED,
|
||||
});
|
||||
return;
|
||||
}
|
||||
setLoading(true);
|
||||
try {
|
||||
const resp = await remoteApi.queryMessageByTopicAndKey(selectedTopic, key);
|
||||
if (resp.status === 0) {
|
||||
setQueryMessageByTopicAndKeyResult(resp.data);
|
||||
if (resp.data.length === 0) {
|
||||
notificationApi.info({
|
||||
message: t.NO_RESULT,
|
||||
description: t.NO_MATCH_RESULT,
|
||||
});
|
||||
}
|
||||
} else {
|
||||
notificationApi.error({
|
||||
message: t.ERROR,
|
||||
description: resp.errMsg || t.QUERY_FAILED,
|
||||
});
|
||||
}
|
||||
} catch (error) {
|
||||
notificationApi.error({
|
||||
message: t.ERROR,
|
||||
description: t.QUERY_FAILED,
|
||||
});
|
||||
console.error("查询失败:", error);
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
// Updated to open the dialog
|
||||
const showMessageDetail = (msgIdToQuery, topicToQuery) => {
|
||||
if (!msgIdToQuery) {
|
||||
notificationApi.warning({
|
||||
message: t.WARNING,
|
||||
description: t.MESSAGE_ID_REQUIRED,
|
||||
});
|
||||
return;
|
||||
}
|
||||
setCurrentMessageIdForDetail(msgIdToQuery);
|
||||
setCurrentTopicForDetail(topicToQuery);
|
||||
setIsMessageDetailModalVisible(true);
|
||||
};
|
||||
|
||||
const handleCloseMessageDetailModal = () => {
|
||||
setIsMessageDetailModalVisible(false);
|
||||
setCurrentMessageIdForDetail(null);
|
||||
setCurrentTopicForDetail(null);
|
||||
};
|
||||
|
||||
const handleResendMessage = async (messageView, consumerGroup) => {
|
||||
setLoading(true); // Set loading for the main page as well, as the dialog itself can't control it
|
||||
let topicToResend = messageView.topic;
|
||||
let msgIdToResend = messageView.msgId;
|
||||
|
||||
|
||||
if (topicToResend.startsWith('%DLQ%')) {
|
||||
if (messageView.properties && messageView.properties.hasOwnProperty("RETRY_TOPIC")) {
|
||||
topicToResend = messageView.properties.RETRY_TOPIC;
|
||||
}
|
||||
if (messageView.properties && messageView.properties.hasOwnProperty("ORIGIN_MESSAGE_ID")) {
|
||||
msgIdToResend = messageView.properties.ORIGIN_MESSAGE_ID;
|
||||
}
|
||||
}
|
||||
|
||||
try {
|
||||
const resp = await remoteApi.resendMessageDirectly(msgIdToResend, consumerGroup, topicToResend);
|
||||
if (resp.status === 0) {
|
||||
notificationApi.success({
|
||||
message: t.SUCCESS,
|
||||
description: t.RESEND_SUCCESS,
|
||||
});
|
||||
} else {
|
||||
notificationApi.error({
|
||||
message: t.ERROR,
|
||||
description: resp.errMsg || t.RESEND_FAILED,
|
||||
});
|
||||
}
|
||||
} catch (error) {
|
||||
notificationApi.error({
|
||||
message: t.ERROR,
|
||||
description: t.RESEND_FAILED,
|
||||
});
|
||||
console.error("重发失败:", error);
|
||||
} finally {
|
||||
setLoading(false);
|
||||
// Optionally, you might want to refresh the message detail after resend
|
||||
// or close the modal if resend was successful and you don't need to see details immediately.
|
||||
// For now, we'll keep the modal open and let the user close it.
|
||||
}
|
||||
};
|
||||
|
||||
const topicColumns = [
|
||||
{
|
||||
title: 'Message ID', dataIndex: 'msgId', key: 'msgId', align: 'center',
|
||||
render: (text) => <Text copyable>{text}</Text>
|
||||
},
|
||||
{title: 'Tag', dataIndex: ['properties', 'TAGS'], key: 'tags', align: 'center'},
|
||||
{title: 'Key', dataIndex: ['properties', 'KEYS'], key: 'keys', align: 'center'},
|
||||
{
|
||||
title: 'StoreTime',
|
||||
dataIndex: 'storeTimestamp',
|
||||
key: 'storeTimestamp',
|
||||
align: 'center',
|
||||
render: (text) => moment(text).format("YYYY-MM-DD HH:mm:ss"),
|
||||
},
|
||||
{
|
||||
title: 'Operation',
|
||||
key: 'operation',
|
||||
align: 'center',
|
||||
render: (_, record) => (
|
||||
<Button type="primary" size="small" onClick={() => showMessageDetail(record.msgId, record.topic)}>
|
||||
{t.MESSAGE_DETAIL}
|
||||
</Button>
|
||||
),
|
||||
},
|
||||
];
|
||||
|
||||
const keyColumns = [
|
||||
{
|
||||
title: 'Message ID', dataIndex: 'msgId', key: 'msgId', align: 'center',
|
||||
render: (text) => <Text copyable>{text}</Text>
|
||||
},
|
||||
{title: 'Tag', dataIndex: ['properties', 'TAGS'], key: 'tags', align: 'center'},
|
||||
{title: 'Key', dataIndex: ['properties', 'KEYS'], key: 'keys', align: 'center'},
|
||||
{
|
||||
title: 'StoreTime',
|
||||
dataIndex: 'storeTimestamp',
|
||||
key: 'storeTimestamp',
|
||||
align: 'center',
|
||||
render: (text) => moment(text).format("YYYY-MM-DD HH:mm:ss"),
|
||||
},
|
||||
{
|
||||
title: 'Operation',
|
||||
key: 'operation',
|
||||
align: 'center',
|
||||
render: (_, record) => (
|
||||
<Button type="primary" size="small" onClick={() => showMessageDetail(record.msgId, record.topic)}>
|
||||
{t.MESSAGE_DETAIL}
|
||||
</Button>
|
||||
),
|
||||
},
|
||||
];
|
||||
|
||||
return (
|
||||
<>
|
||||
{notificationContextHolder}
|
||||
<div style={{padding: '20px'}}>
|
||||
<Spin spinning={loading} tip={t.LOADING_DATA}>
|
||||
<Tabs activeKey={activeTab} onChange={setActiveTab} centered>
|
||||
<TabPane tab="Topic" key="topic">
|
||||
<h5 style={{margin: '15px 0'}}>{t.TOTAL_MESSAGES}</h5>
|
||||
<div style={{padding: '20px', minHeight: '600px'}}>
|
||||
<Form layout="inline" form={form} style={{marginBottom: '20px'}}>
|
||||
<Form.Item label={t.TOPIC}>
|
||||
<Select
|
||||
showSearch
|
||||
style={{width: 300}}
|
||||
placeholder={t.SELECT_TOPIC_PLACEHOLDER}
|
||||
value={selectedTopic}
|
||||
onChange={(value) => {
|
||||
setSelectedTopic(value);
|
||||
onChangeQueryCondition();
|
||||
}}
|
||||
filterOption={(input, option) =>
|
||||
option.children.toLowerCase().indexOf(input.toLowerCase()) >= 0
|
||||
}
|
||||
>
|
||||
{allTopicList.map(topic => (
|
||||
<Option key={topic} value={topic}>{topic}</Option>
|
||||
))}
|
||||
</Select>
|
||||
</Form.Item>
|
||||
<Form.Item label={t.BEGIN}>
|
||||
<DatePicker
|
||||
showTime
|
||||
format="YYYY-MM-DD HH:mm:ss"
|
||||
value={timepickerBegin}
|
||||
onChange={(date) => {
|
||||
setTimepickerBegin(date);
|
||||
onChangeQueryCondition();
|
||||
}}
|
||||
/>
|
||||
</Form.Item>
|
||||
<Form.Item label={t.END}>
|
||||
<DatePicker
|
||||
showTime
|
||||
format="YYYY-MM-DD HH:mm:ss"
|
||||
value={timepickerEnd}
|
||||
onChange={(date) => {
|
||||
setTimepickerEnd(date);
|
||||
onChangeQueryCondition();
|
||||
}}
|
||||
/>
|
||||
</Form.Item>
|
||||
<Form.Item>
|
||||
<Button type="primary" icon={<SearchOutlined/>}
|
||||
onClick={() => queryMessagePageByTopic()}>
|
||||
{t.SEARCH}
|
||||
</Button>
|
||||
</Form.Item>
|
||||
</Form>
|
||||
<Table
|
||||
columns={topicColumns}
|
||||
dataSource={messageShowList}
|
||||
rowKey="msgId"
|
||||
bordered
|
||||
pagination={{
|
||||
current: paginationConf.current,
|
||||
pageSize: paginationConf.pageSize,
|
||||
total: paginationConf.total,
|
||||
onChange: (page, pageSize) => queryMessagePageByTopic(page, pageSize),
|
||||
}}
|
||||
locale={{emptyText: t.NO_MATCH_RESULT}}
|
||||
/>
|
||||
</div>
|
||||
</TabPane>
|
||||
<TabPane tab="Message Key" key="messageKey">
|
||||
<h5 style={{margin: '15px 0'}}>{t.ONLY_RETURN_64_MESSAGES}</h5>
|
||||
<div style={{padding: '20px', minHeight: '600px'}}>
|
||||
<Form layout="inline" style={{marginBottom: '20px'}}>
|
||||
<Form.Item label="Topic:">
|
||||
<Select
|
||||
showSearch
|
||||
style={{width: 300}}
|
||||
placeholder={t.SELECT_TOPIC_PLACEHOLDER}
|
||||
value={selectedTopic}
|
||||
onChange={setSelectedTopic}
|
||||
filterOption={(input, option) =>
|
||||
option.children.toLowerCase().indexOf(input.toLowerCase()) >= 0
|
||||
}
|
||||
>
|
||||
{allTopicList.map(topic => (
|
||||
<Option key={topic} value={topic}>{topic}</Option>
|
||||
))}
|
||||
</Select>
|
||||
</Form.Item>
|
||||
<Form.Item label="Key:">
|
||||
<Input
|
||||
style={{width: 450}}
|
||||
value={key}
|
||||
onChange={(e) => setKey(e.target.value)}
|
||||
/>
|
||||
</Form.Item>
|
||||
<Form.Item>
|
||||
<Button type="primary" icon={<SearchOutlined/>}
|
||||
onClick={queryMessageByTopicAndKey}>
|
||||
{t.SEARCH}
|
||||
</Button>
|
||||
</Form.Item>
|
||||
</Form>
|
||||
<Table
|
||||
columns={keyColumns}
|
||||
dataSource={queryMessageByTopicAndKeyResult}
|
||||
rowKey="msgId"
|
||||
bordered
|
||||
pagination={false}
|
||||
locale={{emptyText: t.NO_MATCH_RESULT}}
|
||||
/>
|
||||
</div>
|
||||
</TabPane>
|
||||
<TabPane tab="Message ID" key="messageId">
|
||||
<h5 style={{margin: '15px 0'}}>
|
||||
{t.MESSAGE_ID_TOPIC_HINT}
|
||||
</h5>
|
||||
<div style={{padding: '20px', minHeight: '600px'}}>
|
||||
<Form layout="inline" style={{marginBottom: '20px'}}>
|
||||
<Form.Item label="Topic:">
|
||||
<Select
|
||||
showSearch
|
||||
style={{width: 300}}
|
||||
placeholder={t.SELECT_TOPIC_PLACEHOLDER}
|
||||
value={selectedTopic}
|
||||
onChange={setSelectedTopic}
|
||||
filterOption={(input, option) =>
|
||||
option.children.toLowerCase().indexOf(input.toLowerCase()) >= 0
|
||||
}
|
||||
>
|
||||
{allTopicList.map(topic => (
|
||||
<Option key={topic} value={topic}>{topic}</Option>
|
||||
))}
|
||||
</Select>
|
||||
</Form.Item>
|
||||
<Form.Item label="MessageId:">
|
||||
<Input
|
||||
style={{width: 450}}
|
||||
value={messageId}
|
||||
onChange={(e) => setMessageId(e.target.value)}
|
||||
/>
|
||||
</Form.Item>
|
||||
<Form.Item>
|
||||
<Button type="primary" icon={<SearchOutlined/>}
|
||||
onClick={() => showMessageDetail(messageId, selectedTopic)}>
|
||||
{t.SEARCH}
|
||||
</Button>
|
||||
</Form.Item>
|
||||
</Form>
|
||||
{/* Message ID 查询结果通常直接弹窗显示,这里不需要表格 */}
|
||||
</div>
|
||||
</TabPane>
|
||||
</Tabs>
|
||||
</Spin>
|
||||
|
||||
{/* Message Detail Dialog Component */}
|
||||
<MessageDetailViewDialog
|
||||
visible={isMessageDetailModalVisible}
|
||||
onCancel={handleCloseMessageDetailModal}
|
||||
messageId={currentMessageIdForDetail}
|
||||
topic={currentTopicForDetail}
|
||||
onResendMessage={handleResendMessage} // Pass the resend function
|
||||
/>
|
||||
</div>
|
||||
</>
|
||||
|
||||
);
|
||||
};
|
||||
|
||||
export default MessageQueryPage;
|
429
frontend-new/src/pages/MessageTrace/messagetrace.jsx
Normal file
429
frontend-new/src/pages/MessageTrace/messagetrace.jsx
Normal file
@@ -0,0 +1,429 @@
|
||||
/*
|
||||
* Licensed to the Apache Software Foundation (ASF) under one or more
|
||||
* contributor license agreements. See the NOTICE file distributed with
|
||||
* this work for additional information regarding copyright ownership.
|
||||
* The ASF licenses this file to You under the Apache License, Version 2.0
|
||||
* (the "License"); you may not use this file except in compliance with
|
||||
* the License. You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import React, {useEffect, useState} from 'react';
|
||||
import {Button, Form, Input, notification, Select, Spin, Table, Tabs, Typography} from 'antd';
|
||||
import moment from 'moment';
|
||||
import {SearchOutlined} from '@ant-design/icons';
|
||||
import {useLanguage} from '../../i18n/LanguageContext';
|
||||
import MessageTraceDetailViewDialog from "../../components/MessageTraceDetailViewDialog";
|
||||
import {remoteApi} from '../../api/remoteApi/remoteApi'; // Import the remoteApi
|
||||
|
||||
const {TabPane} = Tabs;
|
||||
const {Option} = Select;
|
||||
const {Text, Paragraph} = Typography;
|
||||
|
||||
const MessageTraceQueryPage = () => {
|
||||
const {t} = useLanguage();
|
||||
const [activeTab, setActiveTab] = useState('messageKey');
|
||||
const [form] = Form.useForm();
|
||||
const [loading, setLoading] = useState(false);
|
||||
|
||||
// 轨迹主题选择
|
||||
const [allTraceTopicList, setAllTraceTopicList] = useState([]);
|
||||
const [selectedTraceTopic, setSelectedTraceTopic] = useState(null); // Initialize as null or a default trace topic if applicable
|
||||
|
||||
// Topic 查询状态
|
||||
const [allTopicList, setAllTopicList] = useState([]);
|
||||
const [selectedTopic, setSelectedTopic] = useState(null);
|
||||
const [key, setKey] = useState('');
|
||||
const [queryMessageByTopicAndKeyResult, setQueryMessageByTopicAndKeyResult] = useState([]);
|
||||
|
||||
// Message ID 查询状态
|
||||
const [messageId, setMessageId] = useState('');
|
||||
const [queryMessageByMessageIdResult, setQueryMessageByMessageIdResult] = useState([]);
|
||||
|
||||
// State for MessageTraceDetailViewDialog
|
||||
const [isTraceDetailViewOpen, setIsTraceDetailViewOpen] = useState(false);
|
||||
const [traceDetailData, setTraceDetailData] = useState(null);
|
||||
const [notificationApi, notificationContextHolder] = notification.useNotification();
|
||||
|
||||
useEffect(() => {
|
||||
const fetchTopics = async () => {
|
||||
setLoading(true);
|
||||
try {
|
||||
const resp = await remoteApi.queryTopic(true);
|
||||
|
||||
if (resp.status === 0) {
|
||||
const topics = resp.data.topicList.sort();
|
||||
setAllTopicList(topics);
|
||||
|
||||
const traceTopics = topics.filter(topic =>
|
||||
!topic.startsWith('%RETRY%') && !topic.startsWith('%DLQ%')
|
||||
);
|
||||
setAllTraceTopicList(traceTopics);
|
||||
// Optionally set a default trace topic if available, e.g., 'RMQ_SYS_TRACE_TOPIC'
|
||||
if (traceTopics.includes('RMQ_SYS_TRACE_TOPIC')) {
|
||||
setSelectedTraceTopic('RMQ_SYS_TRACE_TOPIC');
|
||||
} else if (traceTopics.length > 0) {
|
||||
setSelectedTraceTopic(traceTopics[0]); // Select the first one if no default
|
||||
}
|
||||
} else {
|
||||
notificationApi.error({
|
||||
message: t.ERROR,
|
||||
description: resp.errMsg || t.QUERY_FAILED,
|
||||
});
|
||||
}
|
||||
} catch (error) {
|
||||
notificationApi.error({
|
||||
message: t.ERROR,
|
||||
description: error.message || t.QUERY_FAILED,
|
||||
});
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
fetchTopics();
|
||||
}, [t]);
|
||||
|
||||
const queryMessageByTopicAndKey = async () => {
|
||||
if (!selectedTopic || !key) {
|
||||
notificationApi.warning({
|
||||
message: t.WARNING,
|
||||
description: t.TOPIC_AND_KEY_REQUIRED,
|
||||
});
|
||||
return;
|
||||
}
|
||||
setLoading(true);
|
||||
|
||||
try {
|
||||
const data = await remoteApi.queryMessageByTopicAndKey(selectedTopic, key);
|
||||
if (data.status === 0) {
|
||||
setQueryMessageByTopicAndKeyResult(data.data);
|
||||
if (data.data.length === 0) {
|
||||
notificationApi.info({
|
||||
message: t.NO_RESULT,
|
||||
description: t.NO_MATCH_RESULT,
|
||||
});
|
||||
}
|
||||
} else {
|
||||
notificationApi.error({
|
||||
message: t.ERROR,
|
||||
description: data.errMsg || t.QUERY_FAILED,
|
||||
});
|
||||
setQueryMessageByTopicAndKeyResult([]); // Clear previous results on error
|
||||
}
|
||||
} catch (error) {
|
||||
notificationApi.error({
|
||||
message: t.ERROR,
|
||||
description: error.message || t.QUERY_FAILED,
|
||||
});
|
||||
setQueryMessageByTopicAndKeyResult([]); // Clear previous results on error
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
const queryMessageByMessageId = async (msgIdToQuery, topicToQuery) => {
|
||||
if (!msgIdToQuery) {
|
||||
notificationApi.warning({
|
||||
message: t.WARNING,
|
||||
description: t.MESSAGE_ID_REQUIRED,
|
||||
});
|
||||
return;
|
||||
}
|
||||
setLoading(true);
|
||||
|
||||
try {
|
||||
const res = await remoteApi.queryMessageByMessageId(msgIdToQuery, topicToQuery);
|
||||
if (res.status === 0) {
|
||||
// 确保 data.data.messageView 存在,并将其包装成数组
|
||||
setQueryMessageByMessageIdResult(res.data && res.data.messageView ? [res.data.messageView] : []);
|
||||
} else {
|
||||
notificationApi.error({
|
||||
message: t.ERROR,
|
||||
description: res.errMsg || t.QUERY_FAILED,
|
||||
});
|
||||
setQueryMessageByMessageIdResult([]); // 清除错误时的旧数据
|
||||
}
|
||||
} catch (error) {
|
||||
notificationApi.error({
|
||||
message: t.ERROR,
|
||||
description: error.message || t.QUERY_FAILED,
|
||||
});
|
||||
setQueryMessageByMessageIdResult([]); // 清除错误时的旧数据
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
const queryMessageTraceByMessageId = async (msgId, traceTopic) => {
|
||||
if (!msgId) {
|
||||
notificationApi.warning({
|
||||
message: t.WARNING,
|
||||
description: t.MESSAGE_ID_REQUIRED,
|
||||
});
|
||||
return;
|
||||
}
|
||||
setLoading(true);
|
||||
|
||||
try {
|
||||
const data = await remoteApi.queryMessageTraceByMessageId(msgId, traceTopic || 'RMQ_SYS_TRACE_TOPIC');
|
||||
if (data.status === 0) {
|
||||
setTraceDetailData(data.data);
|
||||
setIsTraceDetailViewOpen(true);
|
||||
} else {
|
||||
notificationApi.error({
|
||||
message: t.ERROR,
|
||||
description: data.errMsg || t.QUERY_FAILED,
|
||||
});
|
||||
setTraceDetailData(null); // Clear previous trace data on error
|
||||
setIsTraceDetailViewOpen(false); // Do not open dialog if data is not available
|
||||
}
|
||||
} catch (error) {
|
||||
notificationApi.error({
|
||||
message: t.ERROR,
|
||||
description: error.message || t.QUERY_FAILED,
|
||||
});
|
||||
setTraceDetailData(null); // Clear previous trace data on error
|
||||
setIsTraceDetailViewOpen(false); // Do not open dialog if data is not available
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
const handleCloseTraceDetailView = () => {
|
||||
setIsTraceDetailViewOpen(false);
|
||||
setTraceDetailData(null);
|
||||
};
|
||||
|
||||
const keyColumns = [
|
||||
{title: 'Message ID', dataIndex: 'msgId', key: 'msgId', align: 'center'},
|
||||
{title: 'Tag', dataIndex: ['properties', 'TAGS'], key: 'tags', align: 'center'},
|
||||
{title: 'Message Key', dataIndex: ['properties', 'KEYS'], key: 'keys', align: 'center'},
|
||||
{
|
||||
title: 'StoreTime',
|
||||
dataIndex: 'storeTimestamp',
|
||||
key: 'storeTimestamp',
|
||||
align: 'center',
|
||||
render: (text) => moment(text).format('YYYY-MM-DD HH:mm:ss'),
|
||||
},
|
||||
{
|
||||
title: 'Operation',
|
||||
key: 'operation',
|
||||
align: 'center',
|
||||
render: (_, record) => (
|
||||
<Button type="primary" size="small"
|
||||
onClick={() => queryMessageTraceByMessageId(record.msgId, selectedTraceTopic)}>
|
||||
{t.MESSAGE_TRACE_DETAIL}
|
||||
</Button>
|
||||
),
|
||||
},
|
||||
];
|
||||
|
||||
const messageIdColumns = [
|
||||
{title: 'Message ID', dataIndex: 'msgId', key: 'msgId', align: 'center'},
|
||||
// 注意:这里的 dataIndex 直接指向了 messageView 内部的属性
|
||||
{title: 'Tag', dataIndex: ['properties', 'TAGS'], key: 'tags', align: 'center'},
|
||||
{title: 'Message Key', dataIndex: ['properties', 'KEYS'], key: 'keys', align: 'center'},
|
||||
{
|
||||
title: 'StoreTime',
|
||||
dataIndex: 'storeTimestamp',
|
||||
key: 'storeTimestamp',
|
||||
align: 'center',
|
||||
render: (text) => moment(text).format('YYYY-MM-DD HH:mm:ss'),
|
||||
},
|
||||
{
|
||||
title: 'Operation',
|
||||
key: 'operation',
|
||||
align: 'center',
|
||||
render: (_, record) => (
|
||||
<Button type="primary" size="small"
|
||||
onClick={() => queryMessageTraceByMessageId(record.msgId, selectedTraceTopic)}>
|
||||
{t.MESSAGE_TRACE_DETAIL}
|
||||
</Button>
|
||||
),
|
||||
},
|
||||
];
|
||||
|
||||
return (
|
||||
<>
|
||||
{notificationContextHolder}
|
||||
<div style={{padding: '20px'}}>
|
||||
<Spin spinning={loading} tip="加载中...">
|
||||
<div style={{marginBottom: '20px', borderBottom: '1px solid #f0f0f0', paddingBottom: '15px'}}>
|
||||
<Form layout="inline">
|
||||
<Form.Item label={<Text strong>{t.TRACE_TOPIC}:</Text>}>
|
||||
<Select
|
||||
showSearch
|
||||
style={{minWidth: 300}}
|
||||
placeholder={t.SELECT_TRACE_TOPIC_PLACEHOLDER}
|
||||
value={selectedTraceTopic}
|
||||
onChange={setSelectedTraceTopic}
|
||||
filterOption={(input, option) =>
|
||||
option.children && option.children.toLowerCase().indexOf(input.toLowerCase()) >= 0
|
||||
}
|
||||
>
|
||||
{allTraceTopicList.map(topic => (
|
||||
<Option key={topic} value={topic}>{topic}</Option>
|
||||
))}
|
||||
</Select>
|
||||
</Form.Item>
|
||||
<Text type="secondary" style={{marginLeft: 10}}>({t.TRACE_TOPIC_HINT})</Text>
|
||||
</Form>
|
||||
</div>
|
||||
|
||||
<Tabs activeKey={activeTab} onChange={setActiveTab} centered>
|
||||
<TabPane tab="Message Key" key="messageKey">
|
||||
<h5 style={{margin: '15px 0'}}>{t.ONLY_RETURN_64_MESSAGES}</h5>
|
||||
<div style={{padding: '20px', minHeight: '600px'}}>
|
||||
<Form layout="inline" form={form} style={{marginBottom: '20px'}}>
|
||||
<Form.Item label="Topic:">
|
||||
<Select
|
||||
showSearch
|
||||
style={{width: 300}}
|
||||
placeholder={t.SELECT_TOPIC_PLACEHOLDER}
|
||||
value={selectedTopic}
|
||||
onChange={setSelectedTopic}
|
||||
required
|
||||
filterOption={(input, option) =>
|
||||
option.children && option.children.toLowerCase().indexOf(input.toLowerCase()) >= 0
|
||||
}
|
||||
>
|
||||
<Option value="">{t.SELECT_TOPIC_PLACEHOLDER}</Option>
|
||||
{allTopicList.map(topic => (
|
||||
<Option key={topic} value={topic}>{topic}</Option>
|
||||
))}
|
||||
</Select>
|
||||
</Form.Item>
|
||||
<Form.Item label="Key:">
|
||||
<Input
|
||||
style={{width: 450}}
|
||||
value={key}
|
||||
onChange={(e) => setKey(e.target.value)}
|
||||
required
|
||||
/>
|
||||
</Form.Item>
|
||||
<Form.Item>
|
||||
<Button type="primary" icon={<SearchOutlined/>}
|
||||
onClick={queryMessageByTopicAndKey}>
|
||||
{t.SEARCH}
|
||||
</Button>
|
||||
</Form.Item>
|
||||
</Form>
|
||||
<Table
|
||||
columns={keyColumns}
|
||||
dataSource={queryMessageByTopicAndKeyResult}
|
||||
rowKey="msgId"
|
||||
bordered
|
||||
pagination={false}
|
||||
locale={{emptyText: t.NO_MATCH_RESULT}}
|
||||
/>
|
||||
</div>
|
||||
</TabPane>
|
||||
<TabPane tab="Message ID" key="messageId">
|
||||
<h5 style={{margin: '15px 0'}}>{t.MESSAGE_ID_TOPIC_HINT}</h5>
|
||||
<div style={{padding: '20px', minHeight: '600px'}}>
|
||||
<Form layout="inline" style={{marginBottom: '20px'}}>
|
||||
<Form.Item label="Topic:">
|
||||
<Select
|
||||
showSearch
|
||||
style={{width: 300}}
|
||||
placeholder={t.SELECT_TOPIC_PLACEHOLDER}
|
||||
value={selectedTopic}
|
||||
onChange={setSelectedTopic}
|
||||
required
|
||||
filterOption={(input, option) => {
|
||||
if (option.children && typeof option.children === 'string') {
|
||||
return option.children.toLowerCase().includes(input.toLowerCase());
|
||||
}
|
||||
return false;
|
||||
}}
|
||||
>
|
||||
<Option value="">{t.SELECT_TOPIC_PLACEHOLDER}</Option>
|
||||
{allTopicList.map(topic => (
|
||||
<Option key={topic} value={topic}>{topic}</Option>
|
||||
))}
|
||||
</Select>
|
||||
</Form.Item>
|
||||
<Form.Item label="MessageId:">
|
||||
<Input
|
||||
style={{width: 450}}
|
||||
value={messageId}
|
||||
onChange={(e) => setMessageId(e.target.value)}
|
||||
required
|
||||
/>
|
||||
</Form.Item>
|
||||
<Form.Item>
|
||||
<Button type="primary" icon={<SearchOutlined/>}
|
||||
onClick={() => queryMessageByMessageId(messageId, selectedTopic)}>
|
||||
{t.SEARCH}
|
||||
</Button>
|
||||
</Form.Item>
|
||||
</Form>
|
||||
<Table
|
||||
columns={messageIdColumns}
|
||||
dataSource={queryMessageByMessageIdResult}
|
||||
rowKey="msgId"
|
||||
bordered
|
||||
pagination={false}
|
||||
locale={{emptyText: t.NO_MATCH_RESULT}}
|
||||
/>
|
||||
</div>
|
||||
</TabPane>
|
||||
</Tabs>
|
||||
</Spin>
|
||||
|
||||
{/* MessageTraceDetailViewDialog as a child component */}
|
||||
{isTraceDetailViewOpen && traceDetailData && (
|
||||
<div style={{
|
||||
position: 'fixed',
|
||||
top: 0,
|
||||
left: 0,
|
||||
right: 0,
|
||||
bottom: 0,
|
||||
backgroundColor: 'rgba(0, 0, 0, 0.5)',
|
||||
display: 'flex',
|
||||
justifyContent: 'center',
|
||||
alignItems: 'center',
|
||||
zIndex: 1000,
|
||||
}}>
|
||||
<div style={{
|
||||
backgroundColor: '#fff',
|
||||
padding: '20px',
|
||||
borderRadius: '8px',
|
||||
width: '80%',
|
||||
maxHeight: '90%',
|
||||
overflowY: 'auto',
|
||||
position: 'relative'
|
||||
}}>
|
||||
<Typography.Title level={4}
|
||||
style={{marginBottom: '20px'}}>{t.MESSAGE_TRACE_DETAIL}</Typography.Title>
|
||||
<Button
|
||||
onClick={handleCloseTraceDetailView}
|
||||
style={{
|
||||
position: 'absolute',
|
||||
top: '20px',
|
||||
right: '20px',
|
||||
}}
|
||||
>
|
||||
{t.CLOSE}
|
||||
</Button>
|
||||
<MessageTraceDetailViewDialog
|
||||
ngDialogData={traceDetailData}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</>
|
||||
|
||||
);
|
||||
};
|
||||
|
||||
export default MessageTraceQueryPage;
|
183
frontend-new/src/pages/Ops/ops.jsx
Normal file
183
frontend-new/src/pages/Ops/ops.jsx
Normal file
@@ -0,0 +1,183 @@
|
||||
/*
|
||||
* Licensed to the Apache Software Foundation (ASF) under one or more
|
||||
* contributor license agreements. See the NOTICE file distributed with
|
||||
* this work for additional information regarding copyright ownership.
|
||||
* The ASF licenses this file to You under the Apache License, Version 2.0
|
||||
* (the "License"); you may not use this file except in compliance with
|
||||
* the License. You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import { Select, Button, Switch, Input, Typography, Space, message } from 'antd';
|
||||
import {remoteApi} from '../../api/remoteApi/remoteApi';
|
||||
|
||||
const { Title } = Typography;
|
||||
const { Option } = Select;
|
||||
|
||||
const Ops = () => {
|
||||
const [namesrvAddrList, setNamesrvAddrList] = useState([]);
|
||||
const [selectedNamesrv, setSelectedNamesrv] = useState('');
|
||||
const [newNamesrvAddr, setNewNamesrvAddr] = useState('');
|
||||
const [useVIPChannel, setUseVIPChannel] = useState(false);
|
||||
const [useTLS, setUseTLS] = useState(false);
|
||||
const [writeOperationEnabled, setWriteOperationEnabled] = useState(true); // Default to true
|
||||
const [messageApi, msgContextHolder] = message.useMessage();
|
||||
useEffect(() => {
|
||||
const fetchOpsData = async () => {
|
||||
const userRole = sessionStorage.getItem("userrole");
|
||||
setWriteOperationEnabled(userRole === null || userRole === "1"); // Assuming "1" means write access
|
||||
|
||||
const resp = await remoteApi.queryOpsHomePage();
|
||||
if (resp.status === 0) {
|
||||
setNamesrvAddrList(resp.data.namesvrAddrList);
|
||||
setUseVIPChannel(resp.data.useVIPChannel);
|
||||
setUseTLS(resp.data.useTLS);
|
||||
setSelectedNamesrv(resp.data.currentNamesrv);
|
||||
} else {
|
||||
messageApi.error(resp.errMsg);
|
||||
}
|
||||
};
|
||||
fetchOpsData();
|
||||
}, []);
|
||||
|
||||
const handleUpdateNameSvrAddr = async () => {
|
||||
if (!selectedNamesrv) {
|
||||
messageApi.warning('请选择一个 NameServer 地址');
|
||||
return;
|
||||
}
|
||||
const resp = await remoteApi.updateNameSvrAddr(selectedNamesrv);
|
||||
if (resp.status === 0) {
|
||||
messageApi.info('UPDATE SUCCESS');
|
||||
} else {
|
||||
messageApi.error(resp.errMsg);
|
||||
}
|
||||
};
|
||||
|
||||
const handleAddNameSvrAddr = async () => {
|
||||
if (!newNamesrvAddr.trim()) {
|
||||
messageApi.warning('请输入新的 NameServer 地址');
|
||||
return;
|
||||
}
|
||||
const resp = await remoteApi.addNameSvrAddr(newNamesrvAddr.trim());
|
||||
if (resp.status === 0) {
|
||||
if (!namesrvAddrList.includes(newNamesrvAddr.trim())) {
|
||||
setNamesrvAddrList([...namesrvAddrList, newNamesrvAddr.trim()]);
|
||||
}
|
||||
setNewNamesrvAddr('');
|
||||
messageApi.info('ADD SUCCESS');
|
||||
} else {
|
||||
messageApi.error(resp.errMsg);
|
||||
}
|
||||
};
|
||||
|
||||
const handleUpdateIsVIPChannel = async (checked) => {
|
||||
setUseVIPChannel(checked); // Optimistic update
|
||||
const resp = await remoteApi.updateIsVIPChannel(checked);
|
||||
if (resp.status === 0) {
|
||||
messageApi.info('UPDATE SUCCESS');
|
||||
} else {
|
||||
messageApi.error(resp.errMsg);
|
||||
setUseVIPChannel(!checked); // Revert on error
|
||||
}
|
||||
};
|
||||
|
||||
const handleUpdateUseTLS = async (checked) => {
|
||||
setUseTLS(checked); // Optimistic update
|
||||
const resp = await remoteApi.updateUseTLS(checked);
|
||||
if (resp.status === 0) {
|
||||
messageApi.info('UPDATE SUCCESS');
|
||||
} else {
|
||||
messageApi.error(resp.errMsg);
|
||||
setUseTLS(!checked); // Revert on error
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
{msgContextHolder}
|
||||
<div style={{padding: 24}}>
|
||||
<div style={{marginBottom: 24}}>
|
||||
<Title level={4}>NameServerAddressList</Title>
|
||||
<Space wrap align="start">
|
||||
<Select
|
||||
style={{minWidth: 400, maxWidth: 500}}
|
||||
value={selectedNamesrv}
|
||||
onChange={setSelectedNamesrv}
|
||||
disabled={!writeOperationEnabled}
|
||||
placeholder="请选择 NameServer 地址"
|
||||
>
|
||||
{namesrvAddrList.map((addr) => (
|
||||
<Option key={addr} value={addr}>
|
||||
{addr}
|
||||
</Option>
|
||||
))}
|
||||
</Select>
|
||||
|
||||
{writeOperationEnabled && (
|
||||
<Button type="primary" onClick={handleUpdateNameSvrAddr}>
|
||||
UPDATE
|
||||
</Button>
|
||||
)}
|
||||
|
||||
{writeOperationEnabled && (
|
||||
<Input.Group compact style={{minWidth: 400}}>
|
||||
<Input
|
||||
style={{width: 300}}
|
||||
placeholder="NamesrvAddr"
|
||||
value={newNamesrvAddr}
|
||||
onChange={(e) => setNewNamesrvAddr(e.target.value)}
|
||||
/>
|
||||
<Button type="primary" onClick={handleAddNameSvrAddr}>
|
||||
ADD
|
||||
</Button>
|
||||
</Input.Group>
|
||||
)}
|
||||
</Space>
|
||||
</div>
|
||||
|
||||
<div style={{marginBottom: 24}}>
|
||||
<Title level={4}>IsUseVIPChannel</Title>
|
||||
<Space align="center">
|
||||
<Switch
|
||||
checked={useVIPChannel}
|
||||
onChange={handleUpdateIsVIPChannel}
|
||||
disabled={!writeOperationEnabled}
|
||||
/>
|
||||
{writeOperationEnabled && (
|
||||
<Button type="primary" onClick={() => handleUpdateIsVIPChannel(useVIPChannel)}>
|
||||
UPDATE
|
||||
</Button>
|
||||
)}
|
||||
</Space>
|
||||
</div>
|
||||
|
||||
<div style={{marginBottom: 24}}>
|
||||
<Title level={4}>useTLS</Title>
|
||||
<Space align="center">
|
||||
<Switch
|
||||
checked={useTLS}
|
||||
onChange={handleUpdateUseTLS}
|
||||
disabled={!writeOperationEnabled}
|
||||
/>
|
||||
{writeOperationEnabled && (
|
||||
<Button type="primary" onClick={() => handleUpdateUseTLS(useTLS)}>
|
||||
UPDATE
|
||||
</Button>
|
||||
)}
|
||||
</Space>
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
|
||||
);
|
||||
};
|
||||
|
||||
export default Ops;
|
143
frontend-new/src/pages/Producer/producer.jsx
Normal file
143
frontend-new/src/pages/Producer/producer.jsx
Normal file
@@ -0,0 +1,143 @@
|
||||
/*
|
||||
* Licensed to the Apache Software Foundation (ASF) under one or more
|
||||
* contributor license agreements. See the NOTICE file distributed with
|
||||
* this work for additional information regarding copyright ownership.
|
||||
* The ASF licenses this file to You under the Apache License, Version 2.0
|
||||
* (the "License"); you may not use this file except in compliance with
|
||||
* the License. You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import React, {useEffect, useState} from 'react';
|
||||
import {Button, Form, Input, message, Select, Table} from 'antd';
|
||||
import {remoteApi} from '../../api/remoteApi/remoteApi';
|
||||
|
||||
const {Option} = Select;
|
||||
|
||||
const ProducerConnectionList = () => {
|
||||
const [form] = Form.useForm();
|
||||
const [allTopicList, setAllTopicList] = useState([]);
|
||||
const [connectionList, setConnectionList] = useState([]);
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [messageApi, msgContextHolder] = message.useMessage();
|
||||
useEffect(() => {
|
||||
const fetchTopicList = async () => {
|
||||
setLoading(true);
|
||||
try {
|
||||
const resp = await remoteApi.queryTopic(true);
|
||||
if (!resp) {
|
||||
messageApi.error("Failed to fetch topic list - no response");
|
||||
return;
|
||||
}
|
||||
if (resp.status === 0) {
|
||||
setAllTopicList(resp.data.topicList.sort());
|
||||
} else {
|
||||
messageApi.error(resp.errMsg || "Failed to fetch topic list");
|
||||
}
|
||||
} catch (error) {
|
||||
messageApi.error("An error occurred while fetching topic list");
|
||||
console.error("Fetch error:", error);
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
fetchTopicList();
|
||||
}, []);
|
||||
|
||||
const onFinish = (values) => {
|
||||
setLoading(true);
|
||||
const {selectedTopic, producerGroup} = values;
|
||||
remoteApi.queryProducerConnection(selectedTopic, producerGroup, (resp) => {
|
||||
if (resp.status === 0) {
|
||||
setConnectionList(resp.data.connectionSet);
|
||||
} else {
|
||||
messageApi.error(resp.errMsg || "Failed to fetch producer connection list");
|
||||
}
|
||||
setLoading(false);
|
||||
});
|
||||
};
|
||||
|
||||
const columns = [
|
||||
{
|
||||
title: 'clientId',
|
||||
dataIndex: 'clientId',
|
||||
key: 'clientId',
|
||||
align: 'center',
|
||||
},
|
||||
{
|
||||
title: 'clientAddr',
|
||||
dataIndex: 'clientAddr',
|
||||
key: 'clientAddr',
|
||||
align: 'center',
|
||||
},
|
||||
{
|
||||
title: 'language',
|
||||
dataIndex: 'language',
|
||||
key: 'language',
|
||||
align: 'center',
|
||||
},
|
||||
{
|
||||
title: 'version',
|
||||
dataIndex: 'versionDesc',
|
||||
key: 'versionDesc',
|
||||
align: 'center',
|
||||
},
|
||||
];
|
||||
|
||||
return (
|
||||
<>
|
||||
{msgContextHolder}
|
||||
<div className="container-fluid" id="deployHistoryList">
|
||||
<Form
|
||||
form={form}
|
||||
layout="inline"
|
||||
onFinish={onFinish}
|
||||
style={{marginBottom: 20}}
|
||||
>
|
||||
<Form.Item label="TOPIC" name="selectedTopic"
|
||||
rules={[{required: true, message: 'Please select a topic!'}]}>
|
||||
<Select
|
||||
showSearch
|
||||
placeholder="Select a topic"
|
||||
style={{width: 300}}
|
||||
optionFilterProp="children"
|
||||
filterOption={(input, option) =>
|
||||
option.children.toLowerCase().indexOf(input.toLowerCase()) >= 0
|
||||
}
|
||||
>
|
||||
{allTopicList.map((topic) => (
|
||||
<Option key={topic} value={topic}>{topic}</Option>
|
||||
))}
|
||||
</Select>
|
||||
</Form.Item>
|
||||
<Form.Item label="PRODUCER_GROUP" name="producerGroup"
|
||||
rules={[{required: true, message: 'Please input producer group!'}]}>
|
||||
<Input style={{width: 300}}/>
|
||||
</Form.Item>
|
||||
<Form.Item>
|
||||
<Button type="primary" htmlType="submit" loading={loading}>
|
||||
<span className="glyphicon glyphicon-search"></span> SEARCH
|
||||
</Button>
|
||||
</Form.Item>
|
||||
</Form>
|
||||
<Table
|
||||
dataSource={connectionList}
|
||||
columns={columns}
|
||||
rowKey="clientId"
|
||||
pagination={false}
|
||||
bordered
|
||||
/>
|
||||
</div>
|
||||
</>
|
||||
|
||||
);
|
||||
};
|
||||
|
||||
export default ProducerConnectionList;
|
181
frontend-new/src/pages/Proxy/proxy.jsx
Normal file
181
frontend-new/src/pages/Proxy/proxy.jsx
Normal file
@@ -0,0 +1,181 @@
|
||||
/*
|
||||
* Licensed to the Apache Software Foundation (ASF) under one or more
|
||||
* contributor license agreements. See the NOTICE file distributed with
|
||||
* this work for additional information regarding copyright ownership.
|
||||
* The ASF licenses this file to You under the Apache License, Version 2.0
|
||||
* (the "License"); you may not use this file except in compliance with
|
||||
* the License. You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import { Modal, Button, Select, Input, Card, Row, Col, notification, Spin } from 'antd';
|
||||
import { useLanguage } from '../../i18n/LanguageContext';
|
||||
import { remoteApi } from "../../api/remoteApi/remoteApi";
|
||||
|
||||
|
||||
const { Option } = Select;
|
||||
|
||||
const ProxyManager = () => {
|
||||
const { t } = useLanguage();
|
||||
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [proxyAddrList, setProxyAddrList] = useState([]);
|
||||
const [selectedProxy, setSelectedProxy] = useState('');
|
||||
const [newProxyAddr, setNewProxyAddr] = useState('');
|
||||
const [allProxyConfig, setAllProxyConfig] = useState({});
|
||||
|
||||
const [showModal, setShowModal] = useState(false); // 控制 Modal 弹窗显示
|
||||
const [writeOperationEnabled, setWriteOperationEnabled] = useState(true); // 写操作权限,默认 true
|
||||
const [notificationApi, notificationContextHolder] = notification.useNotification();
|
||||
|
||||
useEffect(() => {
|
||||
const userRole = sessionStorage.getItem("userrole");
|
||||
const isWriteEnabled = userRole === null || userRole === '1';
|
||||
setWriteOperationEnabled(isWriteEnabled);
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
setLoading(true);
|
||||
remoteApi.queryProxyHomePage((resp) => {
|
||||
setLoading(false);
|
||||
if (resp.status === 0) {
|
||||
const { proxyAddrList, currentProxyAddr } = resp.data;
|
||||
setProxyAddrList(proxyAddrList || []);
|
||||
setSelectedProxy(currentProxyAddr || (proxyAddrList && proxyAddrList.length > 0 ? proxyAddrList[0] : ''));
|
||||
|
||||
if (currentProxyAddr) {
|
||||
localStorage.setItem('proxyAddr', currentProxyAddr);
|
||||
} else if (proxyAddrList && proxyAddrList.length > 0) {
|
||||
localStorage.setItem('proxyAddr', proxyAddrList[0]);
|
||||
}
|
||||
|
||||
} else {
|
||||
notificationApi.error({ message: resp.errMsg || t.FETCH_PROXY_LIST_FAILED, duration: 2 });
|
||||
}
|
||||
});
|
||||
}, [t]);
|
||||
|
||||
const handleSelectChange = (value) => {
|
||||
setSelectedProxy(value);
|
||||
localStorage.setItem('proxyAddr', value);
|
||||
};
|
||||
|
||||
|
||||
const handleAddProxyAddr = () => {
|
||||
if (!newProxyAddr.trim()) {
|
||||
notificationApi.warning({ message: t.INPUT_PROXY_ADDR_REQUIRED || "Please input a new proxy address.", duration: 2 });
|
||||
return;
|
||||
}
|
||||
setLoading(true);
|
||||
remoteApi.addProxyAddr(newProxyAddr.trim(), (resp) => {
|
||||
setLoading(false);
|
||||
if (resp.status === 0) {
|
||||
if (!proxyAddrList.includes(newProxyAddr.trim())) {
|
||||
setProxyAddrList(prevList => [...prevList, newProxyAddr.trim()]);
|
||||
}
|
||||
setNewProxyAddr('');
|
||||
notificationApi.info({ message: t.SUCCESS || "SUCCESS", duration: 2 });
|
||||
} else {
|
||||
notificationApi.error({ message: resp.errMsg || t.ADD_PROXY_FAILED, duration: 2 });
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
return (
|
||||
<Spin spinning={loading} tip={t.LOADING}>
|
||||
<div className="container-fluid" style={{ padding: '24px' }} id="deployHistoryList">
|
||||
<Card
|
||||
title={
|
||||
<div style={{ fontSize: '20px', fontWeight: 'bold' }}>
|
||||
ProxyServerAddressList
|
||||
</div>
|
||||
}
|
||||
bordered={false}
|
||||
>
|
||||
<Row gutter={[16, 16]} align="middle">
|
||||
<Col flex="auto" style={{ minWidth: 300, maxWidth: 500 }}>
|
||||
<Select
|
||||
style={{ width: '100%' }}
|
||||
value={selectedProxy}
|
||||
onChange={handleSelectChange}
|
||||
placeholder={t.SELECT}
|
||||
showSearch
|
||||
filterOption={(input, option) =>
|
||||
option.children.toLowerCase().indexOf(input.toLowerCase()) >= 0
|
||||
}
|
||||
>
|
||||
{proxyAddrList.map(addr => (
|
||||
<Option key={addr} value={addr}>
|
||||
{addr}
|
||||
</Option>
|
||||
))}
|
||||
</Select>
|
||||
</Col>
|
||||
</Row>
|
||||
|
||||
{writeOperationEnabled && (
|
||||
<Row gutter={[16, 16]} align="middle" style={{ marginTop: 16 }}>
|
||||
<Col>
|
||||
<label htmlFor="newProxyAddrInput">ProxyAddr:</label>
|
||||
</Col>
|
||||
<Col>
|
||||
<Input
|
||||
id="newProxyAddrInput"
|
||||
style={{ width: 300 }}
|
||||
value={newProxyAddr}
|
||||
onChange={(e) => setNewProxyAddr(e.target.value)}
|
||||
placeholder={t.INPUT_PROXY_ADDR}
|
||||
/>
|
||||
</Col>
|
||||
<Col>
|
||||
<Button type="primary" onClick={handleAddProxyAddr}>
|
||||
{t.ADD}
|
||||
</Button>
|
||||
</Col>
|
||||
</Row>
|
||||
)}
|
||||
</Card>
|
||||
|
||||
<Modal
|
||||
open={showModal}
|
||||
onCancel={() => setShowModal(false)}
|
||||
title={`${t.PROXY_CONFIG} [${selectedProxy}]`}
|
||||
footer={
|
||||
<div style={{ textAlign: 'center' }}>
|
||||
<Button onClick={() => setShowModal(false)}>{t.CLOSE}</Button>
|
||||
</div>
|
||||
}
|
||||
width={800}
|
||||
bodyStyle={{ maxHeight: '60vh', overflowY: 'auto' }}
|
||||
>
|
||||
<table className="table table-bordered" style={{ width: '100%' }}>
|
||||
<tbody>
|
||||
{Object.entries(allProxyConfig).length > 0 ? (
|
||||
Object.entries(allProxyConfig).map(([key, value]) => (
|
||||
<tr key={key}>
|
||||
<td style={{ fontWeight: 500, width: '30%' }}>{key}</td>
|
||||
<td>{value}</td>
|
||||
</tr>
|
||||
))
|
||||
) : (
|
||||
<tr>
|
||||
<td colSpan="2" style={{ textAlign: 'center' }}>{t.NO_CONFIG_DATA || "No configuration data available."}</td>
|
||||
</tr>
|
||||
)}
|
||||
</tbody>
|
||||
</table>
|
||||
</Modal>
|
||||
</div>
|
||||
</Spin>
|
||||
);
|
||||
};
|
||||
|
||||
export default ProxyManager;
|
705
frontend-new/src/pages/Topic/topic.jsx
Normal file
705
frontend-new/src/pages/Topic/topic.jsx
Normal file
@@ -0,0 +1,705 @@
|
||||
/*
|
||||
* Licensed to the Apache Software Foundation (ASF) under one or more
|
||||
* contributor license agreements. See the NOTICE file distributed with
|
||||
* this work for additional information regarding copyright ownership.
|
||||
* The ASF licenses this file to You under the Apache License, Version 2.0
|
||||
* (the "License"); you may not use this file except in compliance with
|
||||
* the License. You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import React, {useEffect, useState} from 'react';
|
||||
import {Button, Checkbox, Form, Input, message, Popconfirm, Space, Table} from 'antd';
|
||||
import {useLanguage} from '../../i18n/LanguageContext';
|
||||
import {remoteApi} from '../../api/remoteApi/remoteApi';
|
||||
import ResetOffsetResultDialog from "../../components/topic/ResetOffsetResultDialog";
|
||||
import SendResultDialog from "../../components/topic/SendResultDialog";
|
||||
import TopicModifyDialog from "../../components/topic/TopicModifyDialog";
|
||||
import ConsumerViewDialog from "../../components/topic/ConsumerViewDialog";
|
||||
import ConsumerResetOffsetDialog from "../../components/topic/ConsumerResetOffsetDialog";
|
||||
import SkipMessageAccumulateDialog from "../../components/topic/SkipMessageAccumulateDialog";
|
||||
import StatsViewDialog from "../../components/topic/StatsViewDialog";
|
||||
import RouterViewDialog from "../../components/topic/RouterViewDialog";
|
||||
import SendTopicMessageDialog from "../../components/topic/SendTopicMessageDialog";
|
||||
|
||||
|
||||
const DeployHistoryList = () => {
|
||||
const {t} = useLanguage();
|
||||
const [filterStr, setFilterStr] = useState('');
|
||||
const [filterNormal, setFilterNormal] = useState(true);
|
||||
const [filterDelay, setFilterDelay] = useState(false);
|
||||
const [filterFifo, setFilterFifo] = useState(false);
|
||||
const [filterTransaction, setFilterTransaction] = useState(false);
|
||||
const [filterUnspecified, setFilterUnspecified] = useState(false);
|
||||
const [filterRetry, setFilterRetry] = useState(false);
|
||||
const [filterDLQ, setFilterDLQ] = useState(false);
|
||||
const [filterSystem, setFilterSystem] = useState(false);
|
||||
const [rmqVersion, setRmqVersion] = useState(true);
|
||||
const [writeOperationEnabled, setWriteOperationEnabled] = useState(true);
|
||||
|
||||
const [allTopicList, setAllTopicList] = useState([]);
|
||||
const [allMessageTypeList, setAllMessageTypeList] = useState([]);
|
||||
const [topicShowList, setTopicShowList] = useState([]);
|
||||
const [loading, setLoading] = useState(false);
|
||||
|
||||
// Dialog visibility states
|
||||
const [isAddUpdateTopicModalVisible, setIsAddUpdateTopicModalVisible] = useState(false);
|
||||
const [isResetOffsetResultModalVisible, setIsResetOffsetResultModalVisible] = useState(false);
|
||||
const [isSendResultModalVisible, setIsSendResultModalVisible] = useState(false);
|
||||
const [isConsumerViewModalVisible, setIsConsumerViewModalVisible] = useState(false);
|
||||
const [isConsumerResetOffsetModalVisible, setIsConsumerResetOffsetModalVisible] = useState(false);
|
||||
const [isSkipMessageAccumulateModalVisible, setIsSkipMessageAccumulateModalVisible] = useState(false);
|
||||
const [isStatsViewModalVisible, setIsStatsViewModalVisible] = useState(false);
|
||||
const [isRouterViewModalVisible, setIsRouterViewModalVisible] = useState(false);
|
||||
const [isSendTopicMessageModalVisible, setIsSendTopicMessageModalVisible] = useState(false);
|
||||
|
||||
// Data for dialogs
|
||||
const [currentTopicForDialogs, setCurrentTopicForDialogs] = useState('');
|
||||
const [isUpdateMode, setIsUpdateMode] = useState(false);
|
||||
const [resetOffsetResultData, setResetOffsetResultData] = useState(null);
|
||||
const [sendResultData, setSendResultData] = useState(null);
|
||||
const [consumerData, setConsumerData] = useState(null);
|
||||
const [allConsumerGroupList, setAllConsumerGroupList] = useState([]);
|
||||
const [statsData, setStatsData] = useState(null);
|
||||
const [routeData, setRouteData] = useState(null);
|
||||
const [topicModifyData, setTopicModifyData] = useState([]);
|
||||
const [sendTopicMessageData, setSendTopicMessageData] = useState({
|
||||
topic: '',
|
||||
tag: '',
|
||||
key: '',
|
||||
messageBody: '',
|
||||
traceEnabled: false,
|
||||
});
|
||||
const [selectedConsumerGroups, setSelectedConsumerGroups] = useState([]);
|
||||
const [resetOffsetTime, setResetOffsetTime] = useState(new Date());
|
||||
|
||||
const [allClusterNameList, setAllClusterNameList] = useState([]);
|
||||
const [allBrokerNameList, setAllBrokerNameList] = useState([]);
|
||||
const [messageApi, msgContextHolder] = message.useMessage();
|
||||
// Pagination config
|
||||
const [paginationConf, setPaginationConf] = useState({
|
||||
current: 1,
|
||||
pageSize: 10,
|
||||
total: 0,
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
getTopicList();
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
filterList(paginationConf.current);
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [filterStr, filterNormal, filterDelay, filterFifo, filterTransaction,
|
||||
filterUnspecified, filterRetry, filterDLQ, filterSystem, allTopicList]);
|
||||
|
||||
// Close functions for Modals
|
||||
const closeAddUpdateDialog = () => {
|
||||
setIsAddUpdateTopicModalVisible(false);
|
||||
setTopicModifyData([]);
|
||||
};
|
||||
|
||||
const closeResetOffsetResultDialog = () => {
|
||||
setIsResetOffsetResultModalVisible(false);
|
||||
setResetOffsetResultData(null);
|
||||
};
|
||||
|
||||
const closeSendResultDialog = () => {
|
||||
setIsSendResultModalVisible(false);
|
||||
setSendResultData(null);
|
||||
};
|
||||
|
||||
const closeConsumerViewDialog = () => {
|
||||
setIsConsumerViewModalVisible(false);
|
||||
setConsumerData(null);
|
||||
setAllConsumerGroupList([]);
|
||||
};
|
||||
|
||||
const closeConsumerResetOffsetDialog = () => {
|
||||
setIsConsumerResetOffsetModalVisible(false);
|
||||
setSelectedConsumerGroups([]);
|
||||
setResetOffsetTime(new Date());
|
||||
setAllConsumerGroupList([]);
|
||||
};
|
||||
|
||||
const closeSkipMessageAccumulateDialog = () => {
|
||||
setIsSkipMessageAccumulateModalVisible(false);
|
||||
setSelectedConsumerGroups([]);
|
||||
setAllConsumerGroupList([]);
|
||||
};
|
||||
|
||||
const closeStatsViewDialog = () => {
|
||||
setIsStatsViewModalVisible(false);
|
||||
setStatsData(null);
|
||||
};
|
||||
|
||||
const closeRouterViewDialog = () => {
|
||||
setIsRouterViewModalVisible(false);
|
||||
setRouteData(null);
|
||||
};
|
||||
|
||||
const closeSendTopicMessageDialog = () => {
|
||||
setIsSendTopicMessageModalVisible(false);
|
||||
setSendTopicMessageData({topic: '', tag: '', key: '', messageBody: '', traceEnabled: false});
|
||||
};
|
||||
|
||||
const getTopicList = async () => {
|
||||
setLoading(true);
|
||||
try {
|
||||
const result = await remoteApi.queryTopicList();
|
||||
if (result.status === 0) {
|
||||
setAllTopicList(result.data.topicNameList);
|
||||
setAllMessageTypeList(result.data.messageTypeList);
|
||||
setPaginationConf(prev => ({
|
||||
...prev,
|
||||
total: result.data.topicNameList.length
|
||||
}));
|
||||
} else {
|
||||
messageApi.error(result.errMsg);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("Error fetching topic list:", error);
|
||||
messageApi.error("Failed to fetch topic list");
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
|
||||
const filterList = (currentPage) => {
|
||||
const lowExceptStr = filterStr.toLowerCase();
|
||||
const canShowList = allTopicList.filter((topic, index) => {
|
||||
if (filterStr && !topic.toLowerCase().includes(lowExceptStr)) {
|
||||
return false;
|
||||
}
|
||||
return filterByType(topic, allMessageTypeList[index]);
|
||||
});
|
||||
|
||||
const perPage = paginationConf.pageSize;
|
||||
const from = (currentPage - 1) * perPage;
|
||||
const to = (from + perPage) > canShowList.length ? canShowList.length : from + perPage;
|
||||
|
||||
setTopicShowList(canShowList.slice(from, to));
|
||||
setPaginationConf(prev => ({
|
||||
...prev,
|
||||
current: currentPage,
|
||||
total: canShowList.length
|
||||
}));
|
||||
};
|
||||
|
||||
const filterByType = (topic, type) => {
|
||||
if (filterRetry && type.includes("RETRY")) return true;
|
||||
if (filterDLQ && type.includes("DLQ")) return true;
|
||||
if (filterSystem && type.includes("SYSTEM")) return true;
|
||||
if (rmqVersion && filterUnspecified && type.includes("UNSPECIFIED")) return true;
|
||||
if (filterNormal && type.includes("NORMAL")) return true;
|
||||
if (!rmqVersion && filterNormal && type.includes("UNSPECIFIED")) return true;
|
||||
if (rmqVersion && filterDelay && type.includes("DELAY")) return true;
|
||||
if (rmqVersion && filterFifo && type.includes("FIFO")) return true;
|
||||
if (rmqVersion && filterTransaction && type.includes("TRANSACTION")) return true;
|
||||
|
||||
return false;
|
||||
};
|
||||
|
||||
const handleTableChange = (pagination) => {
|
||||
setPaginationConf(pagination);
|
||||
filterList(pagination.current);
|
||||
};
|
||||
|
||||
const openAddUpdateDialog = async (topic, isSys) => {
|
||||
|
||||
setCurrentTopicForDialogs(typeof topic === 'string' ? topic : (topic && topic.name) || '');
|
||||
const isUpdate = typeof topic === 'string' && !!topic; // 如果 topic 是非空字符串,则认为是更新
|
||||
|
||||
setIsUpdateMode(isUpdate);
|
||||
|
||||
try {
|
||||
if (isUpdate) {
|
||||
// topic 已经是字符串
|
||||
const configResult = await remoteApi.getTopicConfig(topic);
|
||||
if (configResult.status === 0) {
|
||||
const dataToSet = Array.isArray(configResult.data) ? configResult.data : [configResult.data];
|
||||
setTopicModifyData(dataToSet.map(item => ({
|
||||
clusterNameList: [],
|
||||
brokerNameList: item.brokerNameList || [],
|
||||
topicName: item.topicName,
|
||||
messageType: item.messageType || 'NORMAL',
|
||||
writeQueueNums: item.writeQueueNums || 8,
|
||||
readQueueNums: item.readQueueNums || 8,
|
||||
perm: item.perm || 7,
|
||||
})));
|
||||
} else {
|
||||
messageApi.error(configResult.errMsg);
|
||||
return;
|
||||
}
|
||||
} else {
|
||||
setTopicModifyData([{
|
||||
clusterNameList: [],
|
||||
brokerNameList: [],
|
||||
topicName: '',
|
||||
messageType: 'NORMAL',
|
||||
writeQueueNums: 8,
|
||||
readQueueNums: 8,
|
||||
perm: 7,
|
||||
}]);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("Error opening add/update dialog:", error);
|
||||
messageApi.error("Failed to open dialog");
|
||||
return;
|
||||
}
|
||||
|
||||
if(!isUpdate){
|
||||
const clusterResult = await remoteApi.getClusterList();
|
||||
if (clusterResult.status === 0) {
|
||||
setAllClusterNameList(Object.keys(clusterResult.data.clusterInfo.clusterAddrTable));
|
||||
setAllBrokerNameList(Object.keys(clusterResult.data.brokerServer));
|
||||
} else {
|
||||
messageApi.error(clusterResult.errMsg);
|
||||
}
|
||||
}
|
||||
setIsAddUpdateTopicModalVisible(true);
|
||||
};
|
||||
|
||||
// Post Topic Request (Add/Update)
|
||||
const postTopicRequest = async (values) => {
|
||||
try {
|
||||
const result = await remoteApi.createOrUpdateTopic(values);
|
||||
if (result.status === 0) {
|
||||
messageApi.success(t.TOPIC_OPERATION_SUCCESS);
|
||||
closeAddUpdateDialog();
|
||||
if(!isUpdateMode) {
|
||||
await getTopicList()
|
||||
}
|
||||
} else {
|
||||
messageApi.error(result.errMsg);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("Error creating/updating topic:", error);
|
||||
messageApi.error("Failed to create/update topic");
|
||||
}
|
||||
};
|
||||
|
||||
// Delete Topic
|
||||
const deleteTopic = async (topicToDelete) => {
|
||||
try {
|
||||
const result = await remoteApi.deleteTopic(topicToDelete);
|
||||
if (result.status === 0) {
|
||||
messageApi.success(`${t.TOPIC} [${topicToDelete}] ${t.DELETED_SUCCESSFULLY}`);
|
||||
setAllTopicList(allTopicList.filter(topic => topic !== topicToDelete));
|
||||
await getTopicList()
|
||||
} else {
|
||||
messageApi.error(result.errMsg);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("Error deleting topic:", error);
|
||||
messageApi.error("Failed to delete topic");
|
||||
}
|
||||
};
|
||||
|
||||
// Open Stats View Dialog
|
||||
const statsView = async (topic) => {
|
||||
setCurrentTopicForDialogs(topic);
|
||||
try {
|
||||
const result = await remoteApi.getTopicStats(topic);
|
||||
if (result.status === 0) {
|
||||
setStatsData(result.data);
|
||||
setIsStatsViewModalVisible(true);
|
||||
} else {
|
||||
messageApi.error(result.errMsg);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("Error fetching stats:", error);
|
||||
messageApi.error("Failed to fetch stats");
|
||||
}
|
||||
};
|
||||
|
||||
// Open Router View Dialog
|
||||
const routerView = async (topic) => {
|
||||
setCurrentTopicForDialogs(topic);
|
||||
try {
|
||||
const result = await remoteApi.getTopicRoute(topic);
|
||||
if (result.status === 0) {
|
||||
setRouteData(result.data);
|
||||
setIsRouterViewModalVisible(true);
|
||||
} else {
|
||||
messageApi.error(result.errMsg);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("Error fetching route:", error);
|
||||
messageApi.error("Failed to fetch route");
|
||||
}
|
||||
};
|
||||
|
||||
// Open Consumer View Dialog
|
||||
const consumerView = async (topic) => {
|
||||
setCurrentTopicForDialogs(topic);
|
||||
try {
|
||||
const result = await remoteApi.getTopicConsumers(topic);
|
||||
if (result.status === 0) {
|
||||
setConsumerData(result.data);
|
||||
setAllConsumerGroupList(Object.keys(result.data));
|
||||
setIsConsumerViewModalVisible(true);
|
||||
} else {
|
||||
messageApi.error(result.errMsg);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("Error fetching consumers:", error);
|
||||
messageApi.error("Failed to fetch consumers");
|
||||
}
|
||||
};
|
||||
|
||||
// Open Consumer Reset Offset Dialog
|
||||
const openConsumerResetOffsetDialog = async (topic) => {
|
||||
setCurrentTopicForDialogs(topic);
|
||||
try {
|
||||
const result = await remoteApi.getTopicConsumerGroups(topic);
|
||||
if (result.status === 0) {
|
||||
if (!result.data.groupList) {
|
||||
messageApi.error("No consumer groups found");
|
||||
return;
|
||||
}
|
||||
setAllConsumerGroupList(result.data.groupList);
|
||||
setIsConsumerResetOffsetModalVisible(true);
|
||||
} else {
|
||||
messageApi.error(result.errMsg);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("Error fetching consumer groups:", error);
|
||||
messageApi.error("Failed to fetch consumer groups");
|
||||
}
|
||||
};
|
||||
|
||||
// Open Skip Message Accumulate Dialog
|
||||
const openSkipMessageAccumulateDialog = async (topic) => {
|
||||
setCurrentTopicForDialogs(topic);
|
||||
try {
|
||||
const result = await remoteApi.getTopicConsumerGroups(topic);
|
||||
if (result.status === 0) {
|
||||
if (!result.data.groupList) {
|
||||
messageApi.error("No consumer groups found");
|
||||
return;
|
||||
}
|
||||
setAllConsumerGroupList(result.data.groupList);
|
||||
setIsSkipMessageAccumulateModalVisible(true);
|
||||
} else {
|
||||
messageApi.error(result.errMsg);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("Error fetching consumer groups:", error);
|
||||
messageApi.error("Failed to fetch consumer groups");
|
||||
}
|
||||
};
|
||||
|
||||
// Open Send Topic Message Dialog
|
||||
const openSendTopicMessageDialog = (topic) => {
|
||||
setCurrentTopicForDialogs(topic);
|
||||
setSendTopicMessageData(prev => ({...prev, topic}));
|
||||
setIsSendTopicMessageModalVisible(true);
|
||||
};
|
||||
|
||||
const handleInputChange = (e) => {
|
||||
const {name, value} = e.target;
|
||||
setSendTopicMessageData(prevData => ({
|
||||
...prevData,
|
||||
[name]: value,
|
||||
}));
|
||||
};
|
||||
|
||||
const handleResetOffset = async (consumerGroupList, resetTime) => {
|
||||
try {
|
||||
const result = await remoteApi.resetConsumerOffset({
|
||||
resetTime: resetTime, // 使用传递过来的 resetTime
|
||||
consumerGroupList: consumerGroupList, // 使用传递过来的 consumerGroupList
|
||||
topic: currentTopicForDialogs,
|
||||
force: true
|
||||
});
|
||||
if (result.status === 0) {
|
||||
setResetOffsetResultData(result.data);
|
||||
setIsResetOffsetResultModalVisible(true);
|
||||
setIsConsumerResetOffsetModalVisible(false);
|
||||
} else {
|
||||
messageApi.error(result.errMsg);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("Error resetting offset:", error);
|
||||
messageApi.error("Failed to reset offset");
|
||||
}
|
||||
};
|
||||
|
||||
const handleSkipMessageAccumulate = async (consumerGroupListFromDialog) => {
|
||||
try {
|
||||
const result = await remoteApi.skipMessageAccumulate({
|
||||
resetTime: -1,
|
||||
consumerGroupList: consumerGroupListFromDialog, // 使用子组件传递的 consumerGroupList
|
||||
topic: currentTopicForDialogs, // 使用父组件中管理的 topic
|
||||
force: true
|
||||
});
|
||||
if (result.status === 0) {
|
||||
setResetOffsetResultData(result.data); // 注意这里使用了 setResetOffsetResultData,确认这是你期望的
|
||||
setIsResetOffsetResultModalVisible(true); // 注意这里使用了 setIsResetOffsetResultModalVisible,确认这是你期望的
|
||||
setIsSkipMessageAccumulateModalVisible(false);
|
||||
} else {
|
||||
messageApi.error(result.errMsg);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("Error skipping message accumulate:", error);
|
||||
messageApi.error("Failed to skip message accumulate");
|
||||
}
|
||||
};
|
||||
|
||||
const columns = [
|
||||
{
|
||||
title: t.TOPIC,
|
||||
dataIndex: 'topic',
|
||||
key: 'topic',
|
||||
align: 'center',
|
||||
render: (text) => {
|
||||
const sysFlag = text.startsWith('%SYS%');
|
||||
const topic = sysFlag ? text.substring(5) : text;
|
||||
return <span style={{color: sysFlag ? 'red' : ''}}>{topic}</span>;
|
||||
},
|
||||
},
|
||||
{
|
||||
title: t.OPERATION,
|
||||
key: 'operation',
|
||||
align: 'left',
|
||||
render: (_, record) => {
|
||||
const sysFlag = record.topic.startsWith('%SYS%');
|
||||
const topicName = sysFlag ? record.topic.substring(5) : record.topic;
|
||||
return (
|
||||
<Space size="small">
|
||||
<Button type="primary" size="small" onClick={() => statsView(topicName)}>
|
||||
{t.STATUS}
|
||||
</Button>
|
||||
<Button type="primary" size="small" onClick={() => routerView(topicName)}>
|
||||
{t.ROUTER}
|
||||
</Button>
|
||||
<Button type="primary" size="small" onClick={() => consumerView(topicName)}>
|
||||
Consumer {t.MANAGE}
|
||||
</Button>
|
||||
<Button type="primary" size="small" onClick={() => openAddUpdateDialog(topicName, sysFlag)}>
|
||||
Topic {t.CONFIG}
|
||||
</Button>
|
||||
{!sysFlag && (
|
||||
<Button type="primary" size="small" onClick={() => openSendTopicMessageDialog(topicName)}>
|
||||
{t.SEND_MSG}
|
||||
</Button>
|
||||
)}
|
||||
{!sysFlag && writeOperationEnabled && (
|
||||
<Button type="primary" danger size="small"
|
||||
onClick={() => openConsumerResetOffsetDialog(topicName)}>
|
||||
{t.RESET_CUS_OFFSET}
|
||||
</Button>
|
||||
)}
|
||||
{!sysFlag && writeOperationEnabled && (
|
||||
<Button type="primary" danger size="small"
|
||||
onClick={() => openSkipMessageAccumulateDialog(topicName)}>
|
||||
{t.SKIP_MESSAGE_ACCUMULATE}
|
||||
</Button>
|
||||
)}
|
||||
{!sysFlag && writeOperationEnabled && (
|
||||
<Popconfirm
|
||||
title={`${t.ARE_YOU_SURE_TO_DELETE}`}
|
||||
onConfirm={() => deleteTopic(topicName)}
|
||||
okText={t.YES}
|
||||
cancelText={t.NOT}
|
||||
>
|
||||
<Button type="primary" danger size="small">
|
||||
{t.DELETE}
|
||||
</Button>
|
||||
</Popconfirm>
|
||||
)}
|
||||
</Space>
|
||||
);
|
||||
},
|
||||
},
|
||||
];
|
||||
|
||||
return (
|
||||
<>
|
||||
{msgContextHolder}
|
||||
<div className="container-fluid" id="deployHistoryList">
|
||||
<div className="modal-body">
|
||||
<div className="row">
|
||||
<Form layout="inline" className="pull-left col-sm-12">
|
||||
<Form.Item label={t.TOPIC}>
|
||||
<Input
|
||||
value={filterStr}
|
||||
onChange={(e) => setFilterStr(e.target.value)}
|
||||
/>
|
||||
</Form.Item>
|
||||
<Form.Item>
|
||||
<Checkbox checked={filterNormal} onChange={(e) => setFilterNormal(e.target.checked)}>
|
||||
{t.NORMAL}
|
||||
</Checkbox>
|
||||
</Form.Item>
|
||||
{rmqVersion && (
|
||||
<>
|
||||
<Form.Item>
|
||||
<Checkbox checked={filterDelay}
|
||||
onChange={(e) => setFilterDelay(e.target.checked)}>
|
||||
{t.DELAY}
|
||||
</Checkbox>
|
||||
</Form.Item>
|
||||
<Form.Item>
|
||||
<Checkbox checked={filterFifo}
|
||||
onChange={(e) => setFilterFifo(e.target.checked)}>
|
||||
{t.FIFO}
|
||||
</Checkbox>
|
||||
</Form.Item>
|
||||
<Form.Item>
|
||||
<Checkbox checked={filterTransaction}
|
||||
onChange={(e) => setFilterTransaction(e.target.checked)}>
|
||||
{t.TRANSACTION}
|
||||
</Checkbox>
|
||||
</Form.Item>
|
||||
<Form.Item>
|
||||
<Checkbox checked={filterUnspecified}
|
||||
onChange={(e) => setFilterUnspecified(e.target.checked)}>
|
||||
{t.UNSPECIFIED}
|
||||
</Checkbox>
|
||||
</Form.Item>
|
||||
</>
|
||||
)}
|
||||
<Form.Item>
|
||||
<Checkbox checked={filterRetry} onChange={(e) => setFilterRetry(e.target.checked)}>
|
||||
{t.RETRY}
|
||||
</Checkbox>
|
||||
</Form.Item>
|
||||
<Form.Item>
|
||||
<Checkbox checked={filterDLQ} onChange={(e) => setFilterDLQ(e.target.checked)}>
|
||||
{t.DLQ}
|
||||
</Checkbox>
|
||||
</Form.Item>
|
||||
<Form.Item>
|
||||
<Checkbox checked={filterSystem} onChange={(e) => setFilterSystem(e.target.checked)}>
|
||||
{t.SYSTEM}
|
||||
</Checkbox>
|
||||
</Form.Item>
|
||||
{writeOperationEnabled && (
|
||||
<Form.Item>
|
||||
<Button type="primary" onClick={openAddUpdateDialog}>
|
||||
{t.ADD} / {t.UPDATE}
|
||||
</Button>
|
||||
</Form.Item>
|
||||
)}
|
||||
<Form.Item>
|
||||
<Button type="primary" onClick={getTopicList}>
|
||||
{t.REFRESH}
|
||||
</Button>
|
||||
</Form.Item>
|
||||
</Form>
|
||||
</div>
|
||||
<br/>
|
||||
<div>
|
||||
<div className="row">
|
||||
<Table
|
||||
bordered
|
||||
loading={loading}
|
||||
dataSource={topicShowList.map((topic, index) => ({key: index, topic}))}
|
||||
columns={columns}
|
||||
pagination={paginationConf}
|
||||
onChange={handleTableChange}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Modals/Dialogs - 传递 visible 和 onClose prop */}
|
||||
<ResetOffsetResultDialog
|
||||
visible={isResetOffsetResultModalVisible}
|
||||
onClose={closeResetOffsetResultDialog} // 传递关闭函数
|
||||
result={resetOffsetResultData}
|
||||
t={t}
|
||||
/>
|
||||
|
||||
<SendResultDialog
|
||||
visible={isSendResultModalVisible}
|
||||
onClose={closeSendResultDialog} // 传递关闭函数
|
||||
result={sendResultData}
|
||||
t={t}
|
||||
/>
|
||||
|
||||
<TopicModifyDialog
|
||||
visible={isAddUpdateTopicModalVisible}
|
||||
onClose={closeAddUpdateDialog}
|
||||
initialData={topicModifyData}
|
||||
bIsUpdate={isUpdateMode}
|
||||
writeOperationEnabled={writeOperationEnabled}
|
||||
allClusterNameList={allClusterNameList || []}
|
||||
allBrokerNameList={allBrokerNameList || []}
|
||||
onSubmit={postTopicRequest}
|
||||
onInputChange={handleInputChange}
|
||||
t={t}
|
||||
/>
|
||||
|
||||
<ConsumerViewDialog
|
||||
visible={isConsumerViewModalVisible}
|
||||
onClose={closeConsumerViewDialog} // 传递关闭函数
|
||||
topic={currentTopicForDialogs}
|
||||
consumerData={consumerData}
|
||||
consumerGroupCount={allConsumerGroupList.length}
|
||||
t={t}
|
||||
/>
|
||||
|
||||
<ConsumerResetOffsetDialog
|
||||
visible={isConsumerResetOffsetModalVisible}
|
||||
onClose={closeConsumerResetOffsetDialog} // 传递关闭函数
|
||||
topic={currentTopicForDialogs}
|
||||
allConsumerGroupList={allConsumerGroupList}
|
||||
handleResetOffset={handleResetOffset}
|
||||
t={t}
|
||||
/>
|
||||
|
||||
<SkipMessageAccumulateDialog
|
||||
visible={isSkipMessageAccumulateModalVisible}
|
||||
onClose={closeSkipMessageAccumulateDialog} // 传递关闭函数
|
||||
topic={currentTopicForDialogs}
|
||||
allConsumerGroupList={allConsumerGroupList}
|
||||
handleSkipMessageAccumulate={handleSkipMessageAccumulate}
|
||||
t={t}
|
||||
/>
|
||||
|
||||
<StatsViewDialog
|
||||
visible={isStatsViewModalVisible}
|
||||
onClose={closeStatsViewDialog} // 传递关闭函数
|
||||
topic={currentTopicForDialogs}
|
||||
statsData={statsData}
|
||||
t={t}
|
||||
/>
|
||||
|
||||
<RouterViewDialog
|
||||
visible={isRouterViewModalVisible}
|
||||
onClose={closeRouterViewDialog} // 传递关闭函数
|
||||
topic={currentTopicForDialogs}
|
||||
routeData={routeData}
|
||||
t={t}
|
||||
/>
|
||||
|
||||
<SendTopicMessageDialog
|
||||
visible={isSendTopicMessageModalVisible}
|
||||
onClose={closeSendTopicMessageDialog} // 传递关闭函数
|
||||
topic={currentTopicForDialogs}
|
||||
setSendResultData={setSendResultData}
|
||||
setIsSendResultModalVisible={setIsSendResultModalVisible}
|
||||
setIsSendTopicMessageModalVisible={setIsSendTopicMessageModalVisible}
|
||||
sendTopicMessageData={sendTopicMessageData}
|
||||
message={messageApi}
|
||||
t={t}
|
||||
/>
|
||||
</div>
|
||||
</>
|
||||
|
||||
);
|
||||
};
|
||||
|
||||
export default DeployHistoryList;
|
@@ -14,6 +14,7 @@
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
const reportWebVitals = onPerfEntry => {
|
||||
if (onPerfEntry && onPerfEntry instanceof Function) {
|
||||
import('web-vitals').then(({ getCLS, getFID, getFCP, getLCP, getTTFB }) => {
|
262
frontend-new/src/router/index.jsx
Normal file
262
frontend-new/src/router/index.jsx
Normal file
@@ -0,0 +1,262 @@
|
||||
/*
|
||||
* Licensed to the Apache Software Foundation (ASF) under one or more
|
||||
* contributor license agreements. See the NOTICE file distributed with
|
||||
* this work for additional information regarding copyright ownership.
|
||||
* The ASF licenses this file to You under the Apache License, Version 2.0
|
||||
* (the "License"); you may not use this file except in compliance with
|
||||
* the License. You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import React, {useEffect} from 'react';
|
||||
import {HashRouter as Router, Navigate, Route, Routes, useLocation, useNavigate} from 'react-router-dom';
|
||||
import {Layout} from 'antd';
|
||||
import {AnimatePresence, motion} from 'framer-motion';
|
||||
import Login from '../pages/Login/login';
|
||||
import Ops from '../pages/Ops/ops';
|
||||
import Proxy from '../pages/Proxy/proxy';
|
||||
import Cluster from '../pages/Cluster/cluster';
|
||||
import Topic from '../pages/Topic/topic';
|
||||
import Consumer from '../pages/Consumer/consumer';
|
||||
import Producer from '../pages/Producer/producer';
|
||||
import Message from '../pages/Message/message';
|
||||
import DlqMessage from '../pages/DlqMessage/dlqmessage';
|
||||
import MessageTrace from '../pages/MessageTrace/messagetrace';
|
||||
import Acl from '../pages/Acl/acl';
|
||||
|
||||
import Navbar from '../components/Navbar';
|
||||
import DashboardPage from "../pages/Dashboard/DashboardPage";
|
||||
import {remoteApi} from "../api/remoteApi/remoteApi";
|
||||
|
||||
const {Header, Content} = Layout;
|
||||
|
||||
const pageVariants = {
|
||||
initial: {
|
||||
opacity: 0,
|
||||
x: "-100vw"
|
||||
},
|
||||
in: {
|
||||
opacity: 1,
|
||||
x: 0
|
||||
},
|
||||
out: {
|
||||
opacity: 0,
|
||||
x: "100vw"
|
||||
}
|
||||
};
|
||||
|
||||
const pageTransition = {
|
||||
type: "tween",
|
||||
ease: "anticipate",
|
||||
duration: 0.2
|
||||
};
|
||||
|
||||
const AppRouter = () => {
|
||||
const navigate = useNavigate();
|
||||
const location = useLocation();
|
||||
|
||||
useEffect(() => {
|
||||
remoteApi.setRedirectHandler(() => {
|
||||
navigate('/login', { replace: true });
|
||||
});
|
||||
}, [navigate]);
|
||||
|
||||
return (
|
||||
<Layout style={{minHeight: '100vh'}}>
|
||||
<Header style={{padding: 0, height: 'auto', lineHeight: 'normal'}}>
|
||||
<Navbar/>
|
||||
</Header>
|
||||
|
||||
<Content style={{padding: '24px'}}>
|
||||
<AnimatePresence mode="wait">
|
||||
<Routes location={location} key={location.pathname}>
|
||||
<Route
|
||||
path="/login"
|
||||
element={
|
||||
<motion.div
|
||||
variants={pageVariants}
|
||||
initial="initial"
|
||||
animate="in"
|
||||
exit="out"
|
||||
transition={pageTransition}
|
||||
>
|
||||
<Login/>
|
||||
</motion.div>
|
||||
}
|
||||
/>
|
||||
<Route
|
||||
path="/"
|
||||
element={
|
||||
<motion.div
|
||||
variants={pageVariants}
|
||||
initial="initial"
|
||||
animate="in"
|
||||
exit="out"
|
||||
transition={pageTransition}
|
||||
>
|
||||
<DashboardPage/>
|
||||
</motion.div>
|
||||
}
|
||||
/>
|
||||
<Route
|
||||
path="/ops"
|
||||
element={
|
||||
<motion.div
|
||||
variants={pageVariants}
|
||||
initial="initial"
|
||||
animate="in"
|
||||
exit="out"
|
||||
transition={pageTransition}
|
||||
>
|
||||
<Ops/>
|
||||
</motion.div>
|
||||
}
|
||||
/>
|
||||
<Route
|
||||
path="/proxy"
|
||||
element={
|
||||
<motion.div
|
||||
variants={pageVariants}
|
||||
initial="initial"
|
||||
animate="in"
|
||||
exit="out"
|
||||
transition={pageTransition}
|
||||
>
|
||||
<Proxy/>
|
||||
</motion.div>
|
||||
}
|
||||
/>
|
||||
<Route
|
||||
path="/cluster"
|
||||
element={
|
||||
<motion.div
|
||||
variants={pageVariants}
|
||||
initial="initial"
|
||||
animate="in"
|
||||
exit="out"
|
||||
transition={pageTransition}
|
||||
>
|
||||
<Cluster/>
|
||||
</motion.div>
|
||||
}
|
||||
/>
|
||||
<Route
|
||||
path="/topic"
|
||||
element={
|
||||
<motion.div
|
||||
variants={pageVariants}
|
||||
initial="initial"
|
||||
animate="in"
|
||||
exit="out"
|
||||
transition={pageTransition}
|
||||
>
|
||||
<Topic/>
|
||||
</motion.div>
|
||||
}
|
||||
/>
|
||||
<Route
|
||||
path="/consumer"
|
||||
element={
|
||||
<motion.div
|
||||
variants={pageVariants}
|
||||
initial="initial"
|
||||
animate="in"
|
||||
exit="out"
|
||||
transition={pageTransition}
|
||||
>
|
||||
<Consumer/>
|
||||
</motion.div>
|
||||
}
|
||||
/>
|
||||
<Route
|
||||
path="/producer"
|
||||
element={
|
||||
<motion.div
|
||||
variants={pageVariants}
|
||||
initial="initial"
|
||||
animate="in"
|
||||
exit="out"
|
||||
transition={pageTransition}
|
||||
>
|
||||
<Producer/>
|
||||
</motion.div>
|
||||
}
|
||||
/>
|
||||
<Route
|
||||
path="/message"
|
||||
element={
|
||||
<motion.div
|
||||
variants={pageVariants}
|
||||
initial="initial"
|
||||
animate="in"
|
||||
exit="out"
|
||||
transition={pageTransition}
|
||||
>
|
||||
<Message/>
|
||||
</motion.div>
|
||||
}
|
||||
/>
|
||||
<Route
|
||||
path="/dlqMessage"
|
||||
element={
|
||||
<motion.div
|
||||
variants={pageVariants}
|
||||
initial="initial"
|
||||
animate="in"
|
||||
exit="out"
|
||||
transition={pageTransition}
|
||||
>
|
||||
<DlqMessage/>
|
||||
</motion.div>
|
||||
}
|
||||
/>
|
||||
<Route
|
||||
path="/messageTrace"
|
||||
element={
|
||||
<motion.div
|
||||
variants={pageVariants}
|
||||
initial="initial"
|
||||
animate="in"
|
||||
exit="out"
|
||||
transition={pageTransition}
|
||||
>
|
||||
<MessageTrace/>
|
||||
</motion.div>
|
||||
}
|
||||
/>
|
||||
<Route
|
||||
path="/acl"
|
||||
element={
|
||||
<motion.div
|
||||
variants={pageVariants}
|
||||
initial="initial"
|
||||
animate="in"
|
||||
exit="out"
|
||||
transition={pageTransition}
|
||||
>
|
||||
<Acl/>
|
||||
</motion.div>
|
||||
}
|
||||
/>
|
||||
<Route path="*" element={<Navigate to="/"/>}/>
|
||||
</Routes>
|
||||
</AnimatePresence>
|
||||
</Content>
|
||||
</Layout>
|
||||
);
|
||||
};
|
||||
|
||||
const AppWrapper = () => (
|
||||
<Router>
|
||||
<AppRouter/>
|
||||
</Router>
|
||||
);
|
||||
|
||||
export default AppWrapper;
|
@@ -14,6 +14,7 @@
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
// jest-dom adds custom jest matchers for asserting on DOM nodes.
|
||||
// allows you to do things like:
|
||||
// expect(element).toHaveTextContent(/react/i)
|
@@ -15,15 +15,9 @@
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
.login-panel{
|
||||
export const SET_THEME = 'SET_THEME';
|
||||
|
||||
}
|
||||
|
||||
.login-panel .qrcode {
|
||||
width: 100%;
|
||||
hight: 100%;
|
||||
}
|
||||
|
||||
.login-panel .validateCode {
|
||||
/*height:20px;*/
|
||||
}
|
||||
export const setTheme = (themeName) => ({
|
||||
type: SET_THEME,
|
||||
payload: themeName,
|
||||
});
|
39
frontend-new/src/store/context/ThemeContext.js
Normal file
39
frontend-new/src/store/context/ThemeContext.js
Normal file
@@ -0,0 +1,39 @@
|
||||
/*
|
||||
* Licensed to the Apache Software Foundation (ASF) under one or more
|
||||
* contributor license agreements. See the NOTICE file distributed with
|
||||
* this work for additional information regarding copyright ownership.
|
||||
* The ASF licenses this file to You under the Apache License, Version 2.0
|
||||
* (the "License"); you may not use this file except in compliance with
|
||||
* the License. You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
import { useEffect } from 'react';
|
||||
import { useSelector, useDispatch } from 'react-redux';
|
||||
import { themes, defaultTheme } from '../../assets/styles/theme';
|
||||
import { setTheme } from '../actions/themeActions';
|
||||
|
||||
export const useTheme = () => {
|
||||
// 从 Redux store 中取出 currentThemeName
|
||||
const currentThemeName = useSelector(state => state.theme.currentThemeName);
|
||||
const dispatch = useDispatch();
|
||||
|
||||
|
||||
const currentTheme = themes[currentThemeName] || defaultTheme;
|
||||
|
||||
useEffect(() => {
|
||||
localStorage.setItem('appTheme', currentThemeName);
|
||||
}, [currentThemeName]);
|
||||
|
||||
return {
|
||||
currentTheme,
|
||||
currentThemeName,
|
||||
setCurrentThemeName: (themeName) => dispatch(setTheme(themeName)),
|
||||
};
|
||||
};
|
28
frontend-new/src/store/index.js
Normal file
28
frontend-new/src/store/index.js
Normal file
@@ -0,0 +1,28 @@
|
||||
/*
|
||||
* Licensed to the Apache Software Foundation (ASF) under one or more
|
||||
* contributor license agreements. See the NOTICE file distributed with
|
||||
* this work for additional information regarding copyright ownership.
|
||||
* The ASF licenses this file to You under the Apache License, Version 2.0
|
||||
* (the "License"); you may not use this file except in compliance with
|
||||
* the License. You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
import { createStore,combineReducers } from 'redux';
|
||||
import themeReducer from './reducers/themeReducer';
|
||||
|
||||
// 组合所有的 reducers
|
||||
const rootReducer = combineReducers({
|
||||
theme: themeReducer, // theme 状态将通过 state.theme 访问
|
||||
});
|
||||
|
||||
// 创建 Redux store
|
||||
const store = createStore(rootReducer);
|
||||
|
||||
export default store;
|
41
frontend-new/src/store/reducers/themeReducer.js
Normal file
41
frontend-new/src/store/reducers/themeReducer.js
Normal file
@@ -0,0 +1,41 @@
|
||||
/*
|
||||
* Licensed to the Apache Software Foundation (ASF) under one or more
|
||||
* contributor license agreements. See the NOTICE file distributed with
|
||||
* this work for additional information regarding copyright ownership.
|
||||
* The ASF licenses this file to You under the Apache License, Version 2.0
|
||||
* (the "License"); you may not use this file except in compliance with
|
||||
* the License. You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import { SET_THEME } from '../actions/themeActions';
|
||||
|
||||
const getInitialTheme = () => {
|
||||
return localStorage.getItem('appTheme') || 'default';
|
||||
};
|
||||
|
||||
const initialState = {
|
||||
currentThemeName: getInitialTheme(),
|
||||
};
|
||||
|
||||
const themeReducer = (state = initialState, action) => {
|
||||
switch (action.type) {
|
||||
case SET_THEME:
|
||||
// 注意:reducer 应该返回新的状态对象,而不是直接修改旧状态
|
||||
return {
|
||||
...state,
|
||||
currentThemeName: action.payload,
|
||||
};
|
||||
default:
|
||||
return state;
|
||||
}
|
||||
};
|
||||
|
||||
export default themeReducer;
|
16464
frontend/package-lock.json
generated
16464
frontend/package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@@ -1,40 +0,0 @@
|
||||
{
|
||||
"name": "frontend",
|
||||
"version": "0.1.0",
|
||||
"private": true,
|
||||
"dependencies": {
|
||||
"@testing-library/jest-dom": "^5.11.4",
|
||||
"@testing-library/react": "^11.1.0",
|
||||
"@testing-library/user-event": "^12.1.10",
|
||||
"react": "^17.0.2",
|
||||
"react-dom": "^17.0.2",
|
||||
"react-json-view": "^1.21.3",
|
||||
"react-scripts": "4.0.3",
|
||||
"web-vitals": "^1.0.1"
|
||||
},
|
||||
"proxy": "http://localhost:8080",
|
||||
"scripts": {
|
||||
"start": "react-scripts start",
|
||||
"build": "react-scripts build",
|
||||
"test": "react-scripts test",
|
||||
"eject": "react-scripts eject"
|
||||
},
|
||||
"eslintConfig": {
|
||||
"extends": [
|
||||
"react-app",
|
||||
"react-app/jest"
|
||||
]
|
||||
},
|
||||
"browserslist": {
|
||||
"production": [
|
||||
">0.2%",
|
||||
"not dead",
|
||||
"not op_mini all"
|
||||
],
|
||||
"development": [
|
||||
"last 1 chrome version",
|
||||
"last 1 firefox version",
|
||||
"last 1 safari version"
|
||||
]
|
||||
}
|
||||
}
|
Binary file not shown.
Before Width: | Height: | Size: 3.8 KiB |
@@ -1,59 +0,0 @@
|
||||
<!--
|
||||
~ 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.
|
||||
-->
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<link rel="icon" href="%PUBLIC_URL%/favicon.ico" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||
<meta name="theme-color" content="#000000" />
|
||||
<meta
|
||||
name="description"
|
||||
content="Web site created using create-react-app"
|
||||
/>
|
||||
<link rel="apple-touch-icon" href="%PUBLIC_URL%/logo192.png" />
|
||||
<!--
|
||||
manifest.json provides metadata used when your web app is installed on a
|
||||
user's mobile device or desktop. See https://developers.google.com/web/fundamentals/web-app-manifest/
|
||||
-->
|
||||
<link rel="manifest" href="%PUBLIC_URL%/manifest.json" />
|
||||
<!--
|
||||
Notice the use of %PUBLIC_URL% in the tags above.
|
||||
It will be replaced with the URL of the `public` folder during the build.
|
||||
Only files inside the `public` folder can be referenced from the HTML.
|
||||
|
||||
Unlike "/favicon.ico" or "favicon.ico", "%PUBLIC_URL%/favicon.ico" will
|
||||
work correctly both with client-side routing and a non-root public URL.
|
||||
Learn how to configure a non-root public URL by running `npm run build`.
|
||||
-->
|
||||
<title>React App</title>
|
||||
</head>
|
||||
<body>
|
||||
<noscript>You need to enable JavaScript to run this app.</noscript>
|
||||
<div id="root"></div>
|
||||
<!--
|
||||
This HTML file is a template.
|
||||
If you open it directly in the browser, you will see an empty page.
|
||||
|
||||
You can add webfonts, meta tags, or analytics to this file.
|
||||
The build step will place the bundled scripts into the <body> tag.
|
||||
|
||||
To begin the development, run `npm start` or `yarn start`.
|
||||
To create a production bundle, use `npm run build` or `yarn build`.
|
||||
-->
|
||||
</body>
|
||||
</html>
|
Binary file not shown.
Before Width: | Height: | Size: 5.2 KiB |
Binary file not shown.
Before Width: | Height: | Size: 9.4 KiB |
@@ -1,49 +0,0 @@
|
||||
/*
|
||||
* Licensed to the Apache Software Foundation (ASF) under one or more
|
||||
* contributor license agreements. See the NOTICE file distributed with
|
||||
* this work for additional information regarding copyright ownership.
|
||||
* The ASF licenses this file to You under the Apache License, Version 2.0
|
||||
* (the "License"); you may not use this file except in compliance with
|
||||
* the License. You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
import React, {useState, useEffect} from 'react';
|
||||
import logo from './logo.svg';
|
||||
import './App.css';
|
||||
|
||||
function App() {
|
||||
const [message, setMessage] = useState("");
|
||||
|
||||
useEffect(() => {
|
||||
fetch('cluster/list.query')
|
||||
.then(response => response.text())
|
||||
.then(message => {
|
||||
setMessage(message);
|
||||
});
|
||||
}, [])
|
||||
|
||||
|
||||
return (
|
||||
<div className="App">
|
||||
<header className="App-header">
|
||||
<img src={logo} className="App-logo" alt="logo" height="60"/>
|
||||
<p>
|
||||
Edit <code>src/App.js</code> and save to reload.
|
||||
</p>
|
||||
</header>
|
||||
<h1>ClusterInfo</h1>
|
||||
<p>
|
||||
{message}
|
||||
</p>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default App;
|
@@ -1,17 +0,0 @@
|
||||
<!--
|
||||
~ 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.
|
||||
-->
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 841.9 595.3"><g fill="#61DAFB"><path d="M666.3 296.5c0-32.5-40.7-63.3-103.1-82.4 14.4-63.6 8-114.2-20.2-130.4-6.5-3.8-14.1-5.6-22.4-5.6v22.3c4.6 0 8.3.9 11.4 2.6 13.6 7.8 19.5 37.5 14.9 75.7-1.1 9.4-2.9 19.3-5.1 29.4-19.6-4.8-41-8.5-63.5-10.9-13.5-18.5-27.5-35.3-41.6-50 32.6-30.3 63.2-46.9 84-46.9V78c-27.5 0-63.5 19.6-99.9 53.6-36.4-33.8-72.4-53.2-99.9-53.2v22.3c20.7 0 51.4 16.5 84 46.6-14 14.7-28 31.4-41.3 49.9-22.6 2.4-44 6.1-63.6 11-2.3-10-4-19.7-5.2-29-4.7-38.2 1.1-67.9 14.6-75.8 3-1.8 6.9-2.6 11.5-2.6V78.5c-8.4 0-16 1.8-22.6 5.6-28.1 16.2-34.4 66.7-19.9 130.1-62.2 19.2-102.7 49.9-102.7 82.3 0 32.5 40.7 63.3 103.1 82.4-14.4 63.6-8 114.2 20.2 130.4 6.5 3.8 14.1 5.6 22.5 5.6 27.5 0 63.5-19.6 99.9-53.6 36.4 33.8 72.4 53.2 99.9 53.2 8.4 0 16-1.8 22.6-5.6 28.1-16.2 34.4-66.7 19.9-130.1 62-19.1 102.5-49.9 102.5-82.3zm-130.2-66.7c-3.7 12.9-8.3 26.2-13.5 39.5-4.1-8-8.4-16-13.1-24-4.6-8-9.5-15.8-14.4-23.4 14.2 2.1 27.9 4.7 41 7.9zm-45.8 106.5c-7.8 13.5-15.8 26.3-24.1 38.2-14.9 1.3-30 2-45.2 2-15.1 0-30.2-.7-45-1.9-8.3-11.9-16.4-24.6-24.2-38-7.6-13.1-14.5-26.4-20.8-39.8 6.2-13.4 13.2-26.8 20.7-39.9 7.8-13.5 15.8-26.3 24.1-38.2 14.9-1.3 30-2 45.2-2 15.1 0 30.2.7 45 1.9 8.3 11.9 16.4 24.6 24.2 38 7.6 13.1 14.5 26.4 20.8 39.8-6.3 13.4-13.2 26.8-20.7 39.9zm32.3-13c5.4 13.4 10 26.8 13.8 39.8-13.1 3.2-26.9 5.9-41.2 8 4.9-7.7 9.8-15.6 14.4-23.7 4.6-8 8.9-16.1 13-24.1zM421.2 430c-9.3-9.6-18.6-20.3-27.8-32 9 .4 18.2.7 27.5.7 9.4 0 18.7-.2 27.8-.7-9 11.7-18.3 22.4-27.5 32zm-74.4-58.9c-14.2-2.1-27.9-4.7-41-7.9 3.7-12.9 8.3-26.2 13.5-39.5 4.1 8 8.4 16 13.1 24 4.7 8 9.5 15.8 14.4 23.4zM420.7 163c9.3 9.6 18.6 20.3 27.8 32-9-.4-18.2-.7-27.5-.7-9.4 0-18.7.2-27.8.7 9-11.7 18.3-22.4 27.5-32zm-74 58.9c-4.9 7.7-9.8 15.6-14.4 23.7-4.6 8-8.9 16-13 24-5.4-13.4-10-26.8-13.8-39.8 13.1-3.1 26.9-5.8 41.2-7.9zm-90.5 125.2c-35.4-15.1-58.3-34.9-58.3-50.6 0-15.7 22.9-35.6 58.3-50.6 8.6-3.7 18-7 27.7-10.1 5.7 19.6 13.2 40 22.5 60.9-9.2 20.8-16.6 41.1-22.2 60.6-9.9-3.1-19.3-6.5-28-10.2zM310 490c-13.6-7.8-19.5-37.5-14.9-75.7 1.1-9.4 2.9-19.3 5.1-29.4 19.6 4.8 41 8.5 63.5 10.9 13.5 18.5 27.5 35.3 41.6 50-32.6 30.3-63.2 46.9-84 46.9-4.5-.1-8.3-1-11.3-2.7zm237.2-76.2c4.7 38.2-1.1 67.9-14.6 75.8-3 1.8-6.9 2.6-11.5 2.6-20.7 0-51.4-16.5-84-46.6 14-14.7 28-31.4 41.3-49.9 22.6-2.4 44-6.1 63.6-11 2.3 10.1 4.1 19.8 5.2 29.1zm38.5-66.7c-8.6 3.7-18 7-27.7 10.1-5.7-19.6-13.2-40-22.5-60.9 9.2-20.8 16.6-41.1 22.2-60.6 9.9 3.1 19.3 6.5 28.1 10.2 35.4 15.1 58.3 34.9 58.3 50.6-.1 15.7-23 35.6-58.4 50.6zM320.8 78.4z"/><circle cx="420.9" cy="296.5" r="45.7"/><path d="M520.5 78.1z"/></g></svg>
|
Before Width: | Height: | Size: 3.4 KiB |
11595
frontend/yarn.lock
11595
frontend/yarn.lock
File diff suppressed because it is too large
Load Diff
70
pom.xml
70
pom.xml
@@ -82,29 +82,29 @@
|
||||
|
||||
<properties>
|
||||
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
|
||||
<maven.compiler.source>1.8</maven.compiler.source>
|
||||
<maven.compiler.target>1.8</maven.compiler.target>
|
||||
|
||||
<maven.compiler.source>17</maven.compiler.source>
|
||||
<maven.compiler.target>17</maven.compiler.target>
|
||||
<maven.test.skip>false</maven.test.skip>
|
||||
<guava.version>29.0-jre</guava.version>
|
||||
<commons-digester.version>2.1</commons-digester.version>
|
||||
<commons-lang.version>2.6</commons-lang.version>
|
||||
<commons-io.version>2.4</commons-io.version>
|
||||
<commons-cli.version>1.2</commons-cli.version>
|
||||
<commons-collections.version>3.2.2</commons-collections.version>
|
||||
<rocketmq.version>5.1.0</rocketmq.version>
|
||||
<rocketmq.version>5.3.3</rocketmq.version>
|
||||
<surefire.version>2.19.1</surefire.version>
|
||||
<aspectj.version>1.9.6</aspectj.version>
|
||||
<lombok.version>1.18.22</lombok.version>
|
||||
<main.basedir>${basedir}/../..</main.basedir>
|
||||
<docker.image.prefix>apacherocketmq</docker.image.prefix>
|
||||
<spring.boot.version>2.6.0</spring.boot.version>
|
||||
<spring.boot.version>3.4.5</spring.boot.version>
|
||||
<mockito-inline.version>3.3.3</mockito-inline.version>
|
||||
<jaxb-api.version>2.3.1</jaxb-api.version>
|
||||
<jakarta.xml.bind-api.version>4.0.0</jakarta.xml.bind-api.version>
|
||||
<commons-pool2.version>2.4.3</commons-pool2.version>
|
||||
<easyexcel.version>2.2.10</easyexcel.version>
|
||||
<asm.version>4.2</asm.version>
|
||||
<junit.version>4.12</junit.version>
|
||||
<snakeyaml.version>1.32</snakeyaml.version>
|
||||
<snakeyaml.version>2.0</snakeyaml.version>
|
||||
<cglib.version>2.2.2</cglib.version>
|
||||
<joor.version>0.9.6</joor.version>
|
||||
<bcpkix-jdk15on.version>1.68</bcpkix-jdk15on.version>
|
||||
@@ -115,6 +115,12 @@
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-starter-web</artifactId>
|
||||
<version>${spring.boot.version}</version>
|
||||
<exclusions>
|
||||
<exclusion>
|
||||
<groupId>javax.annotation</groupId>
|
||||
<artifactId>javax.annotation-api</artifactId>
|
||||
</exclusion>
|
||||
</exclusions>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
@@ -235,9 +241,9 @@
|
||||
<version>${bcpkix-jdk15on.version}</version>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>javax.xml.bind</groupId>
|
||||
<artifactId>jaxb-api</artifactId>
|
||||
<version>${jaxb-api.version}</version>
|
||||
<groupId>jakarta.xml.bind</groupId>
|
||||
<artifactId>jakarta.xml.bind-api</artifactId>
|
||||
<version>${jakarta.xml.bind-api.version}</version>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.projectlombok</groupId>
|
||||
@@ -282,7 +288,7 @@
|
||||
<plugins>
|
||||
<plugin>
|
||||
<artifactId>maven-compiler-plugin</artifactId>
|
||||
<version>3.5.1</version>
|
||||
<version>3.11.0</version>
|
||||
<configuration>
|
||||
<source>${maven.compiler.source}</source>
|
||||
<target>${maven.compiler.target}</target>
|
||||
@@ -296,6 +302,9 @@
|
||||
<version>${lombok.version}</version>
|
||||
</path>
|
||||
</annotationProcessorPaths>
|
||||
<compilerArgs>
|
||||
<arg>-parameters</arg>
|
||||
</compilerArgs>
|
||||
</configuration>
|
||||
</plugin>
|
||||
<plugin>
|
||||
@@ -384,9 +393,9 @@
|
||||
<version>4.3.0</version>
|
||||
<dependencies>
|
||||
<dependency>
|
||||
<groupId>javax.xml.bind</groupId>
|
||||
<artifactId>jaxb-api</artifactId>
|
||||
<version>${jaxb-api.version}</version>
|
||||
<groupId>jakarta.xml.bind</groupId>
|
||||
<artifactId>jakarta.xml.bind-api</artifactId>
|
||||
<version>${jakarta.xml.bind-api.version}</version>
|
||||
</dependency>
|
||||
</dependencies>
|
||||
</plugin>
|
||||
@@ -404,11 +413,11 @@
|
||||
<exclude>docs/**</exclude>
|
||||
<exclude>src/main/resources/static/vendor/**</exclude>
|
||||
<exclude>src/main/resources/static/src/data/**</exclude>
|
||||
<exclude>frontend/node_modules/**</exclude>
|
||||
<exclude>frontend/build/**</exclude>
|
||||
<exclude>frontend/**.json</exclude>
|
||||
<exclude>frontend/**.lock</exclude>
|
||||
<exclude>frontend/public/manifest.json</exclude>
|
||||
<exclude>frontend-new/node_modules/**</exclude>
|
||||
<exclude>frontend-new/build/**</exclude>
|
||||
<exclude>frontend-new/**.json</exclude>
|
||||
<exclude>frontend-new/**.lock</exclude>
|
||||
<exclude>frontend-new/public/manifest.json</exclude>
|
||||
<exclude>package-lock.json</exclude>
|
||||
</excludes>
|
||||
</configuration>
|
||||
@@ -418,37 +427,36 @@
|
||||
<artifactId>frontend-maven-plugin</artifactId>
|
||||
<version>1.11.3</version>
|
||||
<configuration>
|
||||
<workingDirectory>frontend</workingDirectory>
|
||||
<workingDirectory>frontend-new</workingDirectory>
|
||||
<installDirectory>target</installDirectory>
|
||||
</configuration>
|
||||
<executions>
|
||||
<execution>
|
||||
<id>install node and yarn</id>
|
||||
<id>install node </id>
|
||||
<goals>
|
||||
<goal>install-node-and-yarn</goal>
|
||||
<goal>install-node-and-npm</goal>
|
||||
</goals>
|
||||
<configuration>
|
||||
<nodeVersion>v16.2.0</nodeVersion>
|
||||
<yarnVersion>v1.22.10</yarnVersion>
|
||||
<nodeVersion>v18.2.0</nodeVersion>
|
||||
</configuration>
|
||||
</execution>
|
||||
|
||||
<execution>
|
||||
<id>yarn install</id>
|
||||
<id>npm install</id>
|
||||
<goals>
|
||||
<goal>yarn</goal>
|
||||
<goal>npm</goal>
|
||||
</goals>
|
||||
<configuration>
|
||||
<arguments>install</arguments>
|
||||
<arguments>install --legacy-peer-deps</arguments>
|
||||
</configuration>
|
||||
</execution>
|
||||
<execution>
|
||||
<id>yarn build</id>
|
||||
<id>npm build</id>
|
||||
<goals>
|
||||
<goal>yarn</goal>
|
||||
<goal>npm</goal>
|
||||
</goals>
|
||||
<configuration>
|
||||
<arguments>build</arguments>
|
||||
<arguments>run build</arguments>
|
||||
</configuration>
|
||||
</execution>
|
||||
</executions>
|
||||
@@ -461,7 +469,7 @@
|
||||
<configuration>
|
||||
<target>
|
||||
<copy todir="${project.build.directory}/classes/public">
|
||||
<fileset dir="${project.basedir}/frontend/build" />
|
||||
<fileset dir="${project.basedir}/frontend-new/build" />
|
||||
</copy>
|
||||
</target>
|
||||
</configuration>
|
||||
|
@@ -0,0 +1,25 @@
|
||||
/*
|
||||
* 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.admin;
|
||||
|
||||
import org.apache.rocketmq.tools.admin.MQAdminExt;
|
||||
|
||||
@FunctionalInterface
|
||||
public interface MQAdminExtCallback<T> {
|
||||
T doInMQAdminExt(MQAdminExt mqAdminExt) throws Exception;
|
||||
}
|
@@ -16,7 +16,6 @@
|
||||
*/
|
||||
package org.apache.rocketmq.dashboard.admin;
|
||||
|
||||
import java.util.concurrent.atomic.AtomicLong;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.apache.commons.lang3.StringUtils;
|
||||
import org.apache.rocketmq.acl.common.AclClientRPCHook;
|
||||
@@ -26,6 +25,8 @@ import org.apache.rocketmq.remoting.RPCHook;
|
||||
import org.apache.rocketmq.tools.admin.DefaultMQAdminExt;
|
||||
import org.apache.rocketmq.tools.admin.MQAdminExt;
|
||||
|
||||
import java.util.concurrent.atomic.AtomicLong;
|
||||
|
||||
@Slf4j
|
||||
public class MQAdminFactory {
|
||||
private RMQConfigure rmqConfigure;
|
||||
|
@@ -0,0 +1,132 @@
|
||||
/*
|
||||
* 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.admin;
|
||||
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.apache.commons.pool2.impl.GenericObjectPool;
|
||||
import org.apache.commons.pool2.impl.GenericObjectPoolConfig;
|
||||
import org.apache.rocketmq.client.ClientConfig;
|
||||
import org.apache.rocketmq.dashboard.config.RMQConfigure;
|
||||
import org.apache.rocketmq.tools.admin.MQAdminExt;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
import javax.annotation.PreDestroy;
|
||||
import java.util.concurrent.ConcurrentHashMap;
|
||||
import java.util.concurrent.ConcurrentMap;
|
||||
|
||||
@Component
|
||||
@Slf4j
|
||||
public class UserMQAdminPoolManager {
|
||||
|
||||
|
||||
private final ConcurrentMap<String/* userAk */, GenericObjectPool<MQAdminExt>> userPools = new ConcurrentHashMap<>();
|
||||
|
||||
private final ClientConfig baseClientConfig;
|
||||
|
||||
@Autowired
|
||||
public UserMQAdminPoolManager(RMQConfigure rmqConfigure) {
|
||||
this.baseClientConfig = new ClientConfig();
|
||||
this.baseClientConfig.setNamesrvAddr(rmqConfigure.getNamesrvAddr());
|
||||
this.baseClientConfig.setClientCallbackExecutorThreads(rmqConfigure.getClientCallbackExecutorThreads());
|
||||
this.baseClientConfig.setVipChannelEnabled(Boolean.parseBoolean(rmqConfigure.getIsVIPChannel()));
|
||||
this.baseClientConfig.setUseTLS(rmqConfigure.isUseTLS());
|
||||
log.info("UserMQAdminPoolManager initialized with baseClientConfig for NameServer: {}", rmqConfigure.getNamesrvAddr());
|
||||
}
|
||||
|
||||
|
||||
public MQAdminExt borrowMQAdminExt(String userAk, String userSk) throws Exception {
|
||||
GenericObjectPool<MQAdminExt> userPool = userPools.get(userAk);
|
||||
|
||||
if (userPool == null) {
|
||||
log.info("Creating new MQAdminExt pool for user: {}", userAk);
|
||||
GenericObjectPoolConfig poolConfig = new GenericObjectPoolConfig();
|
||||
poolConfig.setMaxTotal(1);
|
||||
poolConfig.setMaxIdle(1);
|
||||
poolConfig.setMinIdle(0);
|
||||
poolConfig.setTestWhileIdle(true);
|
||||
poolConfig.setTimeBetweenEvictionRunsMillis(20000);
|
||||
poolConfig.setMaxWaitMillis(10000);
|
||||
|
||||
UserSpecificMQAdminPooledObjectFactory factory =
|
||||
new UserSpecificMQAdminPooledObjectFactory(baseClientConfig, userAk, userSk);
|
||||
|
||||
GenericObjectPool<MQAdminExt> newUserPool = new GenericObjectPool<>(factory, poolConfig);
|
||||
|
||||
GenericObjectPool<MQAdminExt> existingPool = userPools.putIfAbsent(userAk, newUserPool);
|
||||
if (existingPool != null) {
|
||||
log.warn("Another thread concurrently created MQAdminExt pool for user {}. Shutting down redundant pool.", userAk);
|
||||
newUserPool.close();
|
||||
userPool = existingPool;
|
||||
} else {
|
||||
userPool = newUserPool;
|
||||
log.info("Successfully created and registered MQAdminExt pool for user: {}", userAk);
|
||||
}
|
||||
}
|
||||
|
||||
return userPool.borrowObject();
|
||||
}
|
||||
|
||||
public void returnMQAdminExt(String userAk, MQAdminExt mqAdminExt) {
|
||||
GenericObjectPool<MQAdminExt> userPool = userPools.get(userAk);
|
||||
if (userPool != null) {
|
||||
try {
|
||||
userPool.returnObject(mqAdminExt);
|
||||
log.debug("Returned MQAdminExt object ({}) to pool for user: {}", mqAdminExt, userAk);
|
||||
} catch (Exception e) {
|
||||
log.error("Failed to return MQAdminExt object ({}) for user {}: {}", mqAdminExt, userAk, e.getMessage(), e);
|
||||
if (mqAdminExt != null) {
|
||||
try {
|
||||
mqAdminExt.shutdown();
|
||||
} catch (Exception se) {
|
||||
log.warn("Error shutting down MQAdminExt after failed return: {}", se.getMessage());
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
log.warn("Attempted to return MQAdminExt for non-existent user pool: {}. Shutting down the object directly.", userAk);
|
||||
if (mqAdminExt != null) {
|
||||
try {
|
||||
mqAdminExt.shutdown();
|
||||
} catch (Exception se) {
|
||||
log.warn("Error shutting down MQAdminExt for non-existent pool: {}", se.getMessage());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public void shutdownUserPool(String userAk) {
|
||||
GenericObjectPool<MQAdminExt> userPool = userPools.remove(userAk);
|
||||
if (userPool != null) {
|
||||
userPool.close();
|
||||
log.info("Shutdown and removed MQAdminExt pool for user: {}", userAk);
|
||||
} else {
|
||||
log.warn("Attempted to shut down non-existent user pool: {}", userAk);
|
||||
}
|
||||
}
|
||||
|
||||
@PreDestroy
|
||||
public void shutdownAllPools() {
|
||||
log.info("Shutting down all MQAdminExt user pools...");
|
||||
userPools.forEach((userAk, pool) -> {
|
||||
pool.close();
|
||||
log.info("Shutdown MQAdminExt pool for user: {}", userAk);
|
||||
});
|
||||
userPools.clear();
|
||||
log.info("All MQAdminExt user pools have been shut down.");
|
||||
}
|
||||
}
|
@@ -0,0 +1,112 @@
|
||||
/*
|
||||
* 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.admin;
|
||||
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.apache.commons.lang3.StringUtils;
|
||||
import org.apache.commons.pool2.PooledObject;
|
||||
import org.apache.commons.pool2.PooledObjectFactory;
|
||||
import org.apache.commons.pool2.impl.DefaultPooledObject;
|
||||
import org.apache.rocketmq.acl.common.AclClientRPCHook;
|
||||
import org.apache.rocketmq.acl.common.SessionCredentials;
|
||||
import org.apache.rocketmq.client.ClientConfig;
|
||||
import org.apache.rocketmq.remoting.RPCHook;
|
||||
import org.apache.rocketmq.remoting.protocol.body.ClusterInfo;
|
||||
import org.apache.rocketmq.tools.admin.DefaultMQAdminExt;
|
||||
import org.apache.rocketmq.tools.admin.MQAdminExt;
|
||||
|
||||
import java.util.UUID;
|
||||
import java.util.concurrent.atomic.AtomicLong;
|
||||
|
||||
@Slf4j
|
||||
public class UserSpecificMQAdminPooledObjectFactory implements PooledObjectFactory<MQAdminExt> {
|
||||
|
||||
private final ClientConfig userSpecificClientConfig;
|
||||
private final RPCHook rpcHook;
|
||||
private final String userAk;
|
||||
private final AtomicLong instanceCreationCounter = new AtomicLong(0);
|
||||
|
||||
public UserSpecificMQAdminPooledObjectFactory(ClientConfig baseClientConfig, String userAk, String userSk) {
|
||||
this.userSpecificClientConfig = baseClientConfig.cloneClientConfig();
|
||||
this.userSpecificClientConfig.setInstanceName("MQ_ADMIN_INSTANCE_" + userAk + "_" + UUID.randomUUID());
|
||||
|
||||
if (StringUtils.isNotEmpty(userAk) && StringUtils.isNotEmpty(userSk)) {
|
||||
this.rpcHook = new AclClientRPCHook(new SessionCredentials(userAk, userSk));
|
||||
} else {
|
||||
this.rpcHook = null;
|
||||
}
|
||||
this.userAk = userAk;
|
||||
|
||||
log.info("UserSpecificMQAdminPooledObjectFactory initialized for user: {}", userAk);
|
||||
log.debug("Factory ClientConfig for user {}: {}", userAk, userSpecificClientConfig);
|
||||
}
|
||||
|
||||
@Override
|
||||
public PooledObject<MQAdminExt> makeObject() throws Exception {
|
||||
DefaultMQAdminExt mqAdminExt = new DefaultMQAdminExt(rpcHook);
|
||||
|
||||
mqAdminExt.setAdminExtGroup("MQ_ADMIN_GROUP_FOR_" + userAk + "_" + instanceCreationCounter.getAndIncrement());
|
||||
|
||||
mqAdminExt.start();
|
||||
log.info("Created new MQAdminExt instance ({}) for user {}", mqAdminExt, userAk);
|
||||
return new DefaultPooledObject<>(mqAdminExt);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void destroyObject(PooledObject<MQAdminExt> p) {
|
||||
MQAdminExt mqAdmin = p.getObject();
|
||||
if (mqAdmin != null) {
|
||||
try {
|
||||
mqAdmin.shutdown();
|
||||
} catch (Exception e) {
|
||||
log.warn("Failed to shut down MQAdminExt object ({}) for user {}: {}", p.getObject(), userAk, e.getMessage(), e);
|
||||
}
|
||||
}
|
||||
log.info("Destroyed MQAdminExt object ({}) for user {}", p.getObject(), userAk);
|
||||
}
|
||||
|
||||
|
||||
@Override
|
||||
public boolean validateObject(PooledObject<MQAdminExt> p) {
|
||||
MQAdminExt mqAdmin = p.getObject();
|
||||
if (mqAdmin == null) {
|
||||
log.warn("MQAdminExt object is null or not started for user {}: {}", userAk, mqAdmin);
|
||||
return false;
|
||||
}
|
||||
try {
|
||||
ClusterInfo clusterInfo = mqAdmin.examineBrokerClusterInfo();
|
||||
boolean isValid = clusterInfo != null && !clusterInfo.getBrokerAddrTable().isEmpty();
|
||||
if (!isValid) {
|
||||
log.warn("Validation failed for MQAdminExt object for user {}: ClusterInfo is invalid or empty. ClusterInfo = {}", userAk, clusterInfo);
|
||||
}
|
||||
return isValid;
|
||||
} catch (Exception e) {
|
||||
log.warn("Validation error for MQAdminExt object for user {}: {}", userAk, e.getMessage(), e);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void activateObject(PooledObject<MQAdminExt> p) {
|
||||
log.debug("Activating MQAdminExt object ({}) for user {}", p.getObject(), userAk);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void passivateObject(PooledObject<MQAdminExt> p) {
|
||||
log.debug("Passivating MQAdminExt object ({}) for user {}", p.getObject(), userAk);
|
||||
}
|
||||
}
|
@@ -18,42 +18,114 @@ package org.apache.rocketmq.dashboard.aspect.admin;
|
||||
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.apache.commons.pool2.impl.GenericObjectPool;
|
||||
import org.apache.rocketmq.dashboard.admin.UserMQAdminPoolManager;
|
||||
import org.apache.rocketmq.dashboard.config.RMQConfigure;
|
||||
import org.apache.rocketmq.dashboard.service.client.MQAdminInstance;
|
||||
import org.apache.rocketmq.dashboard.util.UserInfoContext;
|
||||
import org.apache.rocketmq.dashboard.util.WebUtil;
|
||||
import org.apache.rocketmq.remoting.protocol.body.UserInfo;
|
||||
import org.apache.rocketmq.tools.admin.MQAdminExt;
|
||||
import org.aspectj.lang.ProceedingJoinPoint;
|
||||
import org.aspectj.lang.annotation.Around;
|
||||
import org.aspectj.lang.annotation.Aspect;
|
||||
import org.aspectj.lang.annotation.Pointcut;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.stereotype.Service;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
import java.util.HashSet;
|
||||
import java.util.Set;
|
||||
|
||||
@Aspect
|
||||
@Service
|
||||
@Component
|
||||
@Slf4j
|
||||
public class MQAdminAspect {
|
||||
|
||||
@Autowired
|
||||
private UserMQAdminPoolManager userMQAdminPoolManager;
|
||||
|
||||
@Autowired
|
||||
private GenericObjectPool<MQAdminExt> mqAdminExtPool;
|
||||
|
||||
public MQAdminAspect() {
|
||||
@Autowired
|
||||
private RMQConfigure rmqConfigure;
|
||||
|
||||
private static final Set<String> METHODS_TO_CHECK = new HashSet<>();
|
||||
|
||||
static {
|
||||
METHODS_TO_CHECK.add("getUser");
|
||||
METHODS_TO_CHECK.add("examineBrokerClusterInfo");
|
||||
METHODS_TO_CHECK.add("examineConsumerConnectionInfo");
|
||||
METHODS_TO_CHECK.add("examineConsumeStats");
|
||||
METHODS_TO_CHECK.add("examineProducerConnectionInfo");
|
||||
}
|
||||
|
||||
// Pointcut remains the same, targeting methods in MQAdminExtImpl
|
||||
@Pointcut("execution(* org.apache.rocketmq.dashboard.service.client.MQAdminExtImpl..*(..))")
|
||||
public void mQAdminMethodPointCut() {
|
||||
|
||||
}
|
||||
|
||||
@Around(value = "mQAdminMethodPointCut()")
|
||||
@Pointcut("execution(* org.apache.rocketmq.dashboard.service.client.ProxyAdminImpl..*(..))")
|
||||
public void proxyAdminMethodPointCut() {
|
||||
}
|
||||
|
||||
@Around(value = "mQAdminMethodPointCut()||proxyAdminMethodPointCut()")
|
||||
public Object aroundMQAdminMethod(ProceedingJoinPoint joinPoint) throws Throwable {
|
||||
long start = System.currentTimeMillis();
|
||||
Object obj = null;
|
||||
MQAdminExt mqAdminExt = null; // The MQAdminExt instance borrowed from the pool
|
||||
UserInfo currentUserInfo = null; // The user initiating the operation
|
||||
String methodName = joinPoint.getSignature().getName();
|
||||
|
||||
try {
|
||||
MQAdminInstance.createMQAdmin(mqAdminExtPool);
|
||||
obj = joinPoint.proceed();
|
||||
if (isPoolConfigIsolatedByUser(rmqConfigure.isLoginRequired(), methodName)) {
|
||||
currentUserInfo = (UserInfo) UserInfoContext.get(WebUtil.USER_NAME);
|
||||
// 2. Borrow the user-specific MQAdminExt instance.
|
||||
// currentUser.getName() is assumed to be the AccessKey, and currentUser.getPassword() is SecretKey.
|
||||
mqAdminExt = userMQAdminPoolManager.borrowMQAdminExt(currentUserInfo.getUsername(), currentUserInfo.getPassword());
|
||||
|
||||
// 3. Set the borrowed MQAdminExt instance into the ThreadLocal for MQAdminInstance.
|
||||
// This makes it available to MQAdminExtImpl methods.
|
||||
MQAdminInstance.setCurrentMQAdminExt(mqAdminExt);
|
||||
log.debug("MQAdminExt borrowed for user {} and set in ThreadLocal.", currentUserInfo.getUsername());
|
||||
} else {
|
||||
mqAdminExt = mqAdminExtPool.borrowObject(); // Fallback to a default MQAdminExt if no user is provided
|
||||
MQAdminInstance.setCurrentMQAdminExt(mqAdminExt);
|
||||
}
|
||||
// 4. Proceed with the original method execution.
|
||||
return joinPoint.proceed();
|
||||
|
||||
} finally {
|
||||
MQAdminInstance.returnMQAdmin(mqAdminExtPool);
|
||||
log.debug("op=look method={} cost={}", joinPoint.getSignature().getName(), System.currentTimeMillis() - start);
|
||||
|
||||
if (currentUserInfo != null) {
|
||||
if (mqAdminExt != null) {
|
||||
userMQAdminPoolManager.returnMQAdminExt(currentUserInfo.getUsername(), mqAdminExt);
|
||||
MQAdminInstance.clearCurrentMQAdminExt();
|
||||
log.debug("MQAdminExt returned for user {} and cleared from ThreadLocal.", currentUserInfo.getUsername());
|
||||
}
|
||||
log.debug("Operation {} for user {} cost {}ms",
|
||||
methodName,
|
||||
currentUserInfo.getUsername(),
|
||||
System.currentTimeMillis() - start);
|
||||
} else {
|
||||
if (mqAdminExt != null) {
|
||||
mqAdminExtPool.returnObject(mqAdminExt);
|
||||
MQAdminInstance.clearCurrentMQAdminExt();
|
||||
log.debug("MQAdminExt returned to default pool and cleared from ThreadLocal.");
|
||||
}
|
||||
log.debug("Operation {} cost {}ms",
|
||||
methodName,
|
||||
System.currentTimeMillis() - start);
|
||||
}
|
||||
|
||||
}
|
||||
return obj;
|
||||
}
|
||||
|
||||
private boolean isPoolConfigIsolatedByUser(boolean loginRequired, String methodName) {
|
||||
if (!loginRequired) {
|
||||
return false;
|
||||
} else {
|
||||
return !METHODS_TO_CHECK.contains(methodName);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
@@ -17,6 +17,8 @@
|
||||
|
||||
package org.apache.rocketmq.dashboard.config;
|
||||
|
||||
import jakarta.annotation.Resource;
|
||||
import jakarta.servlet.http.HttpServletRequest;
|
||||
import org.apache.rocketmq.dashboard.interceptor.AuthInterceptor;
|
||||
import org.apache.rocketmq.dashboard.model.UserInfo;
|
||||
import org.apache.rocketmq.dashboard.util.WebUtil;
|
||||
@@ -29,16 +31,16 @@ import org.springframework.web.context.request.NativeWebRequest;
|
||||
import org.springframework.web.method.support.HandlerMethodArgumentResolver;
|
||||
import org.springframework.web.method.support.ModelAndViewContainer;
|
||||
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.ViewControllerRegistry;
|
||||
import org.springframework.web.servlet.config.annotation.WebMvcConfigurerAdapter;
|
||||
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
|
||||
|
||||
import javax.annotation.Resource;
|
||||
import javax.servlet.http.HttpServletRequest;
|
||||
import java.util.List;
|
||||
|
||||
@Configuration
|
||||
public class AuthWebMVCConfigurerAdapter extends WebMvcConfigurerAdapter {
|
||||
public class AuthWebMVCConfigurerAdapter implements WebMvcConfigurer {
|
||||
|
||||
@Autowired
|
||||
@Qualifier("authInterceptor")
|
||||
private AuthInterceptor authInterceptor;
|
||||
@@ -50,19 +52,19 @@ public class AuthWebMVCConfigurerAdapter extends WebMvcConfigurerAdapter {
|
||||
public void addInterceptors(InterceptorRegistry registry) {
|
||||
if (configure.isLoginRequired()) {
|
||||
registry.addInterceptor(authInterceptor).addPathPatterns(
|
||||
"/cluster/**",
|
||||
"/consumer/**",
|
||||
"/dashboard/**",
|
||||
"/dlqMessage/**",
|
||||
"/message/**",
|
||||
"/messageTrace/**",
|
||||
"/monitor/**",
|
||||
"/rocketmq/**",
|
||||
"/ops/**",
|
||||
"/producer/**",
|
||||
"/test/**",
|
||||
"/topic/**",
|
||||
"/acl/**");
|
||||
"/cluster/**",
|
||||
"/consumer/**",
|
||||
"/dashboard/**",
|
||||
"/dlqMessage/**",
|
||||
"/message/**",
|
||||
"/messageTrace/**",
|
||||
"/monitor/**",
|
||||
"/rocketmq/**",
|
||||
"/ops/**",
|
||||
"/producer/**",
|
||||
"/test/**",
|
||||
"/topic/**",
|
||||
"/acl/**");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -77,19 +79,30 @@ public class AuthWebMVCConfigurerAdapter extends WebMvcConfigurerAdapter {
|
||||
|
||||
@Override
|
||||
public Object resolveArgument(MethodParameter methodParameter, ModelAndViewContainer modelAndViewContainer,
|
||||
NativeWebRequest nativeWebRequest, WebDataBinderFactory webDataBinderFactory) throws Exception {
|
||||
NativeWebRequest nativeWebRequest, WebDataBinderFactory webDataBinderFactory) throws Exception {
|
||||
UserInfo userInfo = (UserInfo) WebUtil.getValueFromSession((HttpServletRequest) nativeWebRequest.getNativeRequest(),
|
||||
UserInfo.USER_INFO);
|
||||
UserInfo.USER_INFO);
|
||||
if (userInfo != null) {
|
||||
return userInfo;
|
||||
}
|
||||
throw new MissingServletRequestPartException(UserInfo.USER_INFO);
|
||||
}
|
||||
});
|
||||
|
||||
super.addArgumentResolvers(argumentResolvers); //REVIEW ME
|
||||
}
|
||||
|
||||
@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
|
||||
public void addViewControllers(ViewControllerRegistry registry) {
|
||||
registry.addViewController("*.htm").setViewName("forward:/app.html");
|
||||
|
@@ -17,16 +17,17 @@
|
||||
|
||||
package org.apache.rocketmq.dashboard.config;
|
||||
|
||||
import lombok.Data;
|
||||
import org.springframework.boot.context.properties.ConfigurationProperties;
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
|
||||
import java.util.concurrent.ExecutorService;
|
||||
import java.util.concurrent.LinkedBlockingDeque;
|
||||
import java.util.concurrent.ThreadFactory;
|
||||
import java.util.concurrent.ThreadPoolExecutor;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
import java.util.concurrent.atomic.AtomicLong;
|
||||
import lombok.Data;
|
||||
import org.springframework.boot.context.properties.ConfigurationProperties;
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
|
||||
@Configuration
|
||||
@ConfigurationProperties(prefix = "threadpool.config")
|
||||
|
@@ -16,7 +16,8 @@
|
||||
*/
|
||||
package org.apache.rocketmq.dashboard.config;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import lombok.Getter;
|
||||
import lombok.Setter;
|
||||
import org.apache.commons.collections.CollectionUtils;
|
||||
import org.apache.commons.lang3.StringUtils;
|
||||
import org.apache.rocketmq.common.MixAll;
|
||||
@@ -31,68 +32,64 @@ import org.springframework.context.annotation.Configuration;
|
||||
import org.springframework.http.HttpStatus;
|
||||
|
||||
import java.io.File;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
import static org.apache.rocketmq.client.ClientConfig.SEND_MESSAGE_WITH_VIP_CHANNEL_PROPERTY;
|
||||
|
||||
@Setter
|
||||
@Getter
|
||||
@Configuration
|
||||
@ConfigurationProperties(prefix = "rocketmq.config")
|
||||
public class RMQConfigure {
|
||||
|
||||
private Logger logger = LoggerFactory.getLogger(RMQConfigure.class);
|
||||
//use rocketmq.namesrv.addr first,if it is empty,than use system proerty or system env
|
||||
@Getter
|
||||
private volatile String namesrvAddr = System.getProperty(MixAll.NAMESRV_ADDR_PROPERTY, System.getenv(MixAll.NAMESRV_ADDR_ENV));
|
||||
|
||||
@Setter
|
||||
@Getter
|
||||
private volatile String proxyAddr;
|
||||
|
||||
@Getter
|
||||
private volatile String isVIPChannel = System.getProperty(SEND_MESSAGE_WITH_VIP_CHANNEL_PROPERTY, "true");
|
||||
|
||||
|
||||
@Setter
|
||||
private String dataPath = "/tmp/rocketmq-console/data";
|
||||
|
||||
@Getter
|
||||
private boolean enableDashBoardCollect;
|
||||
|
||||
@Setter
|
||||
@Getter
|
||||
private boolean loginRequired = false;
|
||||
|
||||
|
||||
private String accessKey;
|
||||
|
||||
@Setter
|
||||
@Getter
|
||||
private String secretKey;
|
||||
|
||||
@Setter
|
||||
@Getter
|
||||
private boolean useTLS = false;
|
||||
|
||||
@Setter
|
||||
@Getter
|
||||
private Long timeoutMillis;
|
||||
|
||||
@Getter
|
||||
private List<String> namesrvAddrs = new ArrayList<>();
|
||||
|
||||
@Getter
|
||||
private List<String> proxyAddrs = new ArrayList<>();
|
||||
|
||||
public String getAccessKey() {
|
||||
return accessKey;
|
||||
}
|
||||
|
||||
public void setAccessKey(String accessKey) {
|
||||
this.accessKey = accessKey;
|
||||
}
|
||||
|
||||
public String getSecretKey() {
|
||||
return secretKey;
|
||||
}
|
||||
|
||||
public void setSecretKey(String secretKey) {
|
||||
this.secretKey = secretKey;
|
||||
}
|
||||
|
||||
public String getNamesrvAddr() {
|
||||
return namesrvAddr;
|
||||
}
|
||||
|
||||
public List<String> getNamesrvAddrs() {
|
||||
return namesrvAddrs;
|
||||
}
|
||||
|
||||
public List<String> getProxyAddrs() {
|
||||
return this.proxyAddrs;
|
||||
}
|
||||
@Setter
|
||||
@Getter
|
||||
private Integer clientCallbackExecutorThreads = 4;
|
||||
|
||||
public void setProxyAddrs(List<String> proxyAddrs) {
|
||||
this.proxyAddrs = proxyAddrs;
|
||||
@@ -101,14 +98,6 @@ public class RMQConfigure {
|
||||
}
|
||||
}
|
||||
|
||||
public String getProxyAddr() {
|
||||
return proxyAddr;
|
||||
}
|
||||
|
||||
public void setProxyAddr(String proxyAddr) {
|
||||
this.proxyAddr = proxyAddr;
|
||||
}
|
||||
|
||||
public void setNamesrvAddrs(List<String> namesrvAddrs) {
|
||||
this.namesrvAddrs = namesrvAddrs;
|
||||
if (CollectionUtils.isNotEmpty(namesrvAddrs)) {
|
||||
@@ -135,14 +124,6 @@ public class RMQConfigure {
|
||||
return dataPath + File.separator + "dashboard";
|
||||
}
|
||||
|
||||
public void setDataPath(String dataPath) {
|
||||
this.dataPath = dataPath;
|
||||
}
|
||||
|
||||
public String getIsVIPChannel() {
|
||||
return isVIPChannel;
|
||||
}
|
||||
|
||||
public void setIsVIPChannel(String isVIPChannel) {
|
||||
if (StringUtils.isNotBlank(isVIPChannel)) {
|
||||
this.isVIPChannel = isVIPChannel;
|
||||
@@ -151,38 +132,10 @@ public class RMQConfigure {
|
||||
}
|
||||
}
|
||||
|
||||
public boolean isEnableDashBoardCollect() {
|
||||
return enableDashBoardCollect;
|
||||
}
|
||||
|
||||
public void setEnableDashBoardCollect(String enableDashBoardCollect) {
|
||||
this.enableDashBoardCollect = Boolean.valueOf(enableDashBoardCollect);
|
||||
}
|
||||
|
||||
public boolean isLoginRequired() {
|
||||
return loginRequired;
|
||||
}
|
||||
|
||||
public void setLoginRequired(boolean loginRequired) {
|
||||
this.loginRequired = loginRequired;
|
||||
}
|
||||
|
||||
public boolean isUseTLS() {
|
||||
return useTLS;
|
||||
}
|
||||
|
||||
public void setUseTLS(boolean useTLS) {
|
||||
this.useTLS = useTLS;
|
||||
}
|
||||
|
||||
public Long getTimeoutMillis() {
|
||||
return timeoutMillis;
|
||||
}
|
||||
|
||||
public void setTimeoutMillis(Long timeoutMillis) {
|
||||
this.timeoutMillis = timeoutMillis;
|
||||
}
|
||||
|
||||
// Error Page process logic, move to a central configure later
|
||||
@Bean
|
||||
public ErrorPageRegistrar errorPageRegistrar() {
|
||||
|
@@ -14,133 +14,90 @@
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.apache.rocketmq.dashboard.controller;
|
||||
|
||||
import com.google.common.base.Preconditions;
|
||||
import java.util.List;
|
||||
import javax.annotation.Resource;
|
||||
import javax.servlet.http.HttpServletRequest;
|
||||
import org.apache.commons.collections.CollectionUtils;
|
||||
import org.apache.commons.lang3.StringUtils;
|
||||
import org.apache.rocketmq.common.AclConfig;
|
||||
import org.apache.rocketmq.common.PlainAccessConfig;
|
||||
import org.apache.rocketmq.dashboard.config.RMQConfigure;
|
||||
import org.apache.rocketmq.dashboard.model.User;
|
||||
import org.apache.rocketmq.dashboard.model.UserInfo;
|
||||
import org.apache.rocketmq.dashboard.model.request.AclRequest;
|
||||
import org.apache.rocketmq.dashboard.permisssion.Permission;
|
||||
import org.apache.rocketmq.dashboard.service.AclService;
|
||||
import org.apache.rocketmq.dashboard.support.JsonResult;
|
||||
import org.apache.rocketmq.dashboard.util.WebUtil;
|
||||
import org.apache.rocketmq.dashboard.model.PolicyRequest;
|
||||
import org.apache.rocketmq.dashboard.model.request.UserCreateRequest;
|
||||
import org.apache.rocketmq.dashboard.model.request.UserUpdateRequest;
|
||||
import org.apache.rocketmq.dashboard.service.impl.AclServiceImpl;
|
||||
import org.apache.rocketmq.remoting.protocol.body.UserInfo;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.web.bind.annotation.DeleteMapping;
|
||||
import org.springframework.web.bind.annotation.GetMapping;
|
||||
import org.springframework.web.bind.annotation.PostMapping;
|
||||
import org.springframework.web.bind.annotation.RequestBody;
|
||||
import org.springframework.web.bind.annotation.RequestMapping;
|
||||
import org.springframework.web.bind.annotation.RequestMethod;
|
||||
import org.springframework.web.bind.annotation.RequestParam;
|
||||
import org.springframework.web.bind.annotation.ResponseBody;
|
||||
import org.springframework.web.bind.annotation.RestController;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
@RestController
|
||||
@RequestMapping("/acl")
|
||||
@Permission
|
||||
public class AclController {
|
||||
|
||||
@Resource
|
||||
private AclService aclService;
|
||||
@Autowired
|
||||
private AclServiceImpl aclService;
|
||||
|
||||
@Resource
|
||||
private RMQConfigure configure;
|
||||
|
||||
@GetMapping("/enable.query")
|
||||
public Object isEnableAcl() {
|
||||
return new JsonResult<>(configure.isACLEnabled());
|
||||
@GetMapping("/listUsers")
|
||||
@ResponseBody
|
||||
public List<UserInfo> listUsers(@RequestParam(required = false) String brokerAddress) {
|
||||
return aclService.listUsers(brokerAddress);
|
||||
}
|
||||
|
||||
@GetMapping("/config.query")
|
||||
public AclConfig getAclConfig(HttpServletRequest request) {
|
||||
if (!configure.isLoginRequired()) {
|
||||
return aclService.getAclConfig(false);
|
||||
}
|
||||
UserInfo userInfo = (UserInfo) WebUtil.getValueFromSession(request, WebUtil.USER_INFO);
|
||||
// if user info is null but reach here, must exclude secret key for safety.
|
||||
return aclService.getAclConfig(userInfo == null || userInfo.getUser().getType() != User.ADMIN);
|
||||
@GetMapping("/listAcls")
|
||||
@ResponseBody
|
||||
public Object listAcls(
|
||||
@RequestParam(required = false) String brokerAddress,
|
||||
@RequestParam(required = false) String searchParam) {
|
||||
return aclService.listAcls(brokerAddress, searchParam);
|
||||
}
|
||||
|
||||
@PostMapping("/add.do")
|
||||
public Object addAclConfig(@RequestBody PlainAccessConfig config) {
|
||||
Preconditions.checkArgument(StringUtils.isNotEmpty(config.getAccessKey()), "accessKey is null");
|
||||
Preconditions.checkArgument(StringUtils.isNotEmpty(config.getSecretKey()), "secretKey is null");
|
||||
aclService.addAclConfig(config);
|
||||
@PostMapping("/createAcl")
|
||||
@ResponseBody
|
||||
public Object createAcl(@RequestBody PolicyRequest request) {
|
||||
aclService.createAcl(request);
|
||||
return true;
|
||||
}
|
||||
|
||||
@PostMapping("/delete.do")
|
||||
public Object deleteAclConfig(@RequestBody PlainAccessConfig config) {
|
||||
Preconditions.checkArgument(StringUtils.isNotEmpty(config.getAccessKey()), "accessKey is null");
|
||||
aclService.deleteAclConfig(config);
|
||||
@DeleteMapping("/deleteUser")
|
||||
@ResponseBody
|
||||
public Object deleteUser(@RequestParam(required = false) String brokerAddress, @RequestParam String username) {
|
||||
aclService.deleteUser(brokerAddress, username);
|
||||
return true;
|
||||
}
|
||||
|
||||
@PostMapping("/update.do")
|
||||
public Object updateAclConfig(@RequestBody PlainAccessConfig config) {
|
||||
Preconditions.checkArgument(StringUtils.isNotEmpty(config.getSecretKey()), "secretKey is null");
|
||||
aclService.updateAclConfig(config);
|
||||
@RequestMapping(value = "/updateUser", method = RequestMethod.POST, produces = "application/json;charset=UTF-8")
|
||||
@ResponseBody
|
||||
public Object updateUser(@RequestBody UserUpdateRequest request) {
|
||||
aclService.updateUser(request.getBrokerAddress(), request.getUserInfo());
|
||||
return true;
|
||||
}
|
||||
|
||||
@PostMapping("/topic/add.do")
|
||||
public Object addAclTopicConfig(@RequestBody AclRequest request) {
|
||||
Preconditions.checkArgument(StringUtils.isNotEmpty(request.getConfig().getAccessKey()), "accessKey is null");
|
||||
Preconditions.checkArgument(StringUtils.isNotEmpty(request.getConfig().getSecretKey()), "secretKey is null");
|
||||
Preconditions.checkArgument(CollectionUtils.isNotEmpty(request.getConfig().getTopicPerms()), "topic perms is null");
|
||||
Preconditions.checkArgument(StringUtils.isNotEmpty(request.getTopicPerm()), "topic perm is null");
|
||||
aclService.addOrUpdateAclTopicConfig(request);
|
||||
@PostMapping("/createUser")
|
||||
public Object createUser(@RequestBody UserCreateRequest request) {
|
||||
aclService.createUser(request.getBrokerAddress(), request.getUserInfo());
|
||||
return true;
|
||||
}
|
||||
|
||||
@PostMapping("/group/add.do")
|
||||
public Object addAclGroupConfig(@RequestBody AclRequest request) {
|
||||
Preconditions.checkArgument(StringUtils.isNotEmpty(request.getConfig().getAccessKey()), "accessKey is null");
|
||||
Preconditions.checkArgument(StringUtils.isNotEmpty(request.getConfig().getSecretKey()), "secretKey is null");
|
||||
Preconditions.checkArgument(CollectionUtils.isNotEmpty(request.getConfig().getGroupPerms()), "group perms is null");
|
||||
Preconditions.checkArgument(StringUtils.isNotEmpty(request.getGroupPerm()), "group perm is null");
|
||||
aclService.addOrUpdateAclGroupConfig(request);
|
||||
@DeleteMapping("/deleteAcl")
|
||||
public Object deleteAcl(
|
||||
@RequestParam(required = false) String brokerAddress,
|
||||
@RequestParam String subject,
|
||||
@RequestParam(required = false) String resource) {
|
||||
aclService.deleteAcl(brokerAddress, subject, resource);
|
||||
return true;
|
||||
}
|
||||
|
||||
@PostMapping("/perm/delete.do")
|
||||
public Object deletePermConfig(@RequestBody AclRequest request) {
|
||||
Preconditions.checkArgument(StringUtils.isNotEmpty(request.getConfig().getAccessKey()), "accessKey is null");
|
||||
Preconditions.checkArgument(StringUtils.isNotEmpty(request.getConfig().getSecretKey()), "secretKey is null");
|
||||
aclService.deletePermConfig(request);
|
||||
@RequestMapping(value = "/updateAcl", method = RequestMethod.POST, produces = "application/json;charset=UTF-8")
|
||||
@ResponseBody
|
||||
public Object updateAcl(@RequestBody PolicyRequest request) {
|
||||
aclService.updateAcl(request);
|
||||
return true;
|
||||
}
|
||||
|
||||
@PostMapping("/sync.do")
|
||||
public Object syncConfig(@RequestBody PlainAccessConfig config) {
|
||||
Preconditions.checkArgument(StringUtils.isNotEmpty(config.getAccessKey()), "accessKey is null");
|
||||
Preconditions.checkArgument(StringUtils.isNotEmpty(config.getSecretKey()), "secretKey is null");
|
||||
aclService.syncData(config);
|
||||
return true;
|
||||
}
|
||||
|
||||
@PostMapping("/white/list/add.do")
|
||||
public Object addWhiteList(@RequestBody List<String> whiteList) {
|
||||
Preconditions.checkArgument(CollectionUtils.isNotEmpty(whiteList), "white list is null");
|
||||
aclService.addWhiteList(whiteList);
|
||||
return true;
|
||||
}
|
||||
|
||||
@DeleteMapping("/white/list/delete.do")
|
||||
public Object deleteWhiteAddr(@RequestParam String request) {
|
||||
aclService.deleteWhiteAddr(request);
|
||||
return true;
|
||||
}
|
||||
|
||||
@PostMapping("/white/list/sync.do")
|
||||
public Object synchronizeWhiteList(@RequestBody List<String> whiteList) {
|
||||
Preconditions.checkArgument(CollectionUtils.isNotEmpty(whiteList), "white list is null");
|
||||
aclService.synchronizeWhiteList(whiteList);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
@@ -16,14 +16,13 @@
|
||||
*/
|
||||
package org.apache.rocketmq.dashboard.controller;
|
||||
|
||||
import jakarta.annotation.Resource;
|
||||
import org.apache.rocketmq.dashboard.permisssion.Permission;
|
||||
import org.apache.rocketmq.dashboard.service.ClusterService;
|
||||
import org.springframework.stereotype.Controller;
|
||||
import org.springframework.web.bind.annotation.RequestMapping;
|
||||
import org.springframework.web.bind.annotation.RequestMethod;
|
||||
import org.springframework.web.bind.annotation.RequestParam;
|
||||
|
||||
import javax.annotation.Resource;
|
||||
import org.springframework.web.bind.annotation.ResponseBody;
|
||||
|
||||
@Controller
|
||||
|
@@ -17,9 +17,8 @@
|
||||
package org.apache.rocketmq.dashboard.controller;
|
||||
|
||||
import com.google.common.base.Preconditions;
|
||||
import javax.annotation.Resource;
|
||||
import jakarta.annotation.Resource;
|
||||
import org.apache.commons.collections.CollectionUtils;
|
||||
import org.apache.rocketmq.remoting.protocol.body.ConsumerConnection;
|
||||
import org.apache.rocketmq.dashboard.model.ConnectionInfo;
|
||||
import org.apache.rocketmq.dashboard.model.request.ConsumerConfigInfo;
|
||||
import org.apache.rocketmq.dashboard.model.request.DeleteSubGroupRequest;
|
||||
@@ -27,6 +26,7 @@ import org.apache.rocketmq.dashboard.model.request.ResetOffsetRequest;
|
||||
import org.apache.rocketmq.dashboard.permisssion.Permission;
|
||||
import org.apache.rocketmq.dashboard.service.ConsumerService;
|
||||
import org.apache.rocketmq.dashboard.util.JsonUtil;
|
||||
import org.apache.rocketmq.remoting.protocol.body.ConsumerConnection;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
import org.springframework.stereotype.Controller;
|
||||
@@ -51,6 +51,19 @@ public class ConsumerController {
|
||||
return consumerService.queryGroupList(skipSysGroup, address);
|
||||
}
|
||||
|
||||
@RequestMapping(value = "/group.refresh")
|
||||
@ResponseBody
|
||||
public Object refresh(String address,
|
||||
String consumerGroup) {
|
||||
return consumerService.refreshGroup(address, consumerGroup);
|
||||
}
|
||||
|
||||
@RequestMapping(value = "group.refresh.all")
|
||||
@ResponseBody
|
||||
public Object refreshAll(String address) {
|
||||
return consumerService.refreshAllGroup(address);
|
||||
}
|
||||
|
||||
@RequestMapping(value = "/group.query")
|
||||
@ResponseBody
|
||||
public Object groupQuery(@RequestParam String consumerGroup, String address) {
|
||||
|
@@ -17,9 +17,8 @@
|
||||
|
||||
package org.apache.rocketmq.dashboard.controller;
|
||||
|
||||
import javax.annotation.Resource;
|
||||
|
||||
import com.google.common.base.Strings;
|
||||
import jakarta.annotation.Resource;
|
||||
import org.apache.rocketmq.dashboard.permisssion.Permission;
|
||||
import org.apache.rocketmq.dashboard.service.DashboardService;
|
||||
import org.springframework.stereotype.Controller;
|
||||
|
@@ -17,10 +17,8 @@
|
||||
package org.apache.rocketmq.dashboard.controller;
|
||||
|
||||
import com.google.common.collect.Lists;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import javax.annotation.Resource;
|
||||
import javax.servlet.http.HttpServletResponse;
|
||||
import jakarta.annotation.Resource;
|
||||
import jakarta.servlet.http.HttpServletResponse;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.apache.rocketmq.common.MixAll;
|
||||
import org.apache.rocketmq.common.message.MessageExt;
|
||||
@@ -41,6 +39,9 @@ import org.springframework.web.bind.annotation.RequestMethod;
|
||||
import org.springframework.web.bind.annotation.RequestParam;
|
||||
import org.springframework.web.bind.annotation.ResponseBody;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
@Controller
|
||||
@RequestMapping("/dlqMessage")
|
||||
@Permission
|
||||
@@ -61,7 +62,7 @@ public class DlqMessageController {
|
||||
|
||||
@GetMapping(value = "/exportDlqMessage.do")
|
||||
public void exportDlqMessage(HttpServletResponse response, @RequestParam String consumerGroup,
|
||||
@RequestParam String msgId) {
|
||||
@RequestParam String msgId) {
|
||||
MessageExt messageExt = null;
|
||||
try {
|
||||
String topic = MixAll.DLQ_GROUP_TOPIC_PREFIX + consumerGroup;
|
||||
|
@@ -17,6 +17,9 @@
|
||||
|
||||
package org.apache.rocketmq.dashboard.controller;
|
||||
|
||||
import jakarta.annotation.Resource;
|
||||
import jakarta.servlet.http.HttpServletRequest;
|
||||
import jakarta.servlet.http.HttpServletResponse;
|
||||
import org.apache.rocketmq.dashboard.config.RMQConfigure;
|
||||
import org.apache.rocketmq.dashboard.model.LoginInfo;
|
||||
import org.apache.rocketmq.dashboard.model.LoginResult;
|
||||
@@ -32,12 +35,8 @@ import org.springframework.beans.factory.annotation.Value;
|
||||
import org.springframework.stereotype.Controller;
|
||||
import org.springframework.web.bind.annotation.RequestMapping;
|
||||
import org.springframework.web.bind.annotation.RequestMethod;
|
||||
import org.springframework.web.bind.annotation.RequestParam;
|
||||
import org.springframework.web.bind.annotation.ResponseBody;
|
||||
|
||||
import javax.annotation.Resource;
|
||||
import javax.servlet.http.HttpServletRequest;
|
||||
import javax.servlet.http.HttpServletResponse;
|
||||
|
||||
@Controller
|
||||
@RequestMapping("/login")
|
||||
@@ -66,12 +65,11 @@ public class LoginController {
|
||||
|
||||
@RequestMapping(value = "/login.do", method = RequestMethod.POST)
|
||||
@ResponseBody
|
||||
public Object login(@RequestParam("username") String username,
|
||||
@RequestParam(value = "password") String password,
|
||||
HttpServletRequest request,
|
||||
HttpServletResponse response) throws Exception {
|
||||
logger.info("user:{} login", username);
|
||||
User user = userService.queryByUsernameAndPassword(username, password);
|
||||
public Object login(org.apache.rocketmq.remoting.protocol.body.UserInfo userInfoRequest,
|
||||
HttpServletRequest request,
|
||||
HttpServletResponse response) throws Exception {
|
||||
logger.info("user:{} login", userInfoRequest.getUsername());
|
||||
User user = userService.queryByUsernameAndPassword(userInfoRequest.getUsername(), userInfoRequest.getPassword());
|
||||
|
||||
if (user == null) {
|
||||
throw new IllegalArgumentException("Bad username or password!");
|
||||
@@ -79,9 +77,9 @@ public class LoginController {
|
||||
user.setPassword(null);
|
||||
UserInfo userInfo = WebUtil.setLoginInfo(request, response, user);
|
||||
WebUtil.setSessionValue(request, WebUtil.USER_INFO, userInfo);
|
||||
WebUtil.setSessionValue(request, WebUtil.USER_NAME, username);
|
||||
WebUtil.setSessionValue(request, WebUtil.USER_NAME, userInfoRequest.getUsername());
|
||||
userInfo.setSessionId(WebUtil.getSessionId(request));
|
||||
LoginResult result = new LoginResult(username, user.getType(), contextPath);
|
||||
LoginResult result = new LoginResult(userInfoRequest.getUsername(), user.getType(), contextPath);
|
||||
return result;
|
||||
}
|
||||
}
|
||||
|
@@ -17,26 +17,26 @@
|
||||
package org.apache.rocketmq.dashboard.controller;
|
||||
|
||||
import com.google.common.collect.Maps;
|
||||
import jakarta.annotation.Resource;
|
||||
import org.apache.rocketmq.common.Pair;
|
||||
import org.apache.rocketmq.remoting.protocol.body.ConsumeMessageDirectlyResult;
|
||||
import org.apache.rocketmq.dashboard.model.MessagePage;
|
||||
import org.apache.rocketmq.dashboard.model.MessageView;
|
||||
import org.apache.rocketmq.dashboard.model.request.MessageQuery;
|
||||
import org.apache.rocketmq.dashboard.permisssion.Permission;
|
||||
import org.apache.rocketmq.dashboard.service.MessageService;
|
||||
import org.apache.rocketmq.dashboard.util.JsonUtil;
|
||||
import org.apache.rocketmq.remoting.protocol.body.ConsumeMessageDirectlyResult;
|
||||
import org.apache.rocketmq.tools.admin.api.MessageTrack;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
import org.springframework.stereotype.Controller;
|
||||
import org.springframework.web.bind.annotation.RequestMapping;
|
||||
import org.springframework.web.bind.annotation.ResponseBody;
|
||||
import org.springframework.web.bind.annotation.RequestParam;
|
||||
import org.springframework.web.bind.annotation.RequestMethod;
|
||||
import org.springframework.web.bind.annotation.PostMapping;
|
||||
import org.springframework.web.bind.annotation.RequestBody;
|
||||
import org.springframework.web.bind.annotation.RequestMapping;
|
||||
import org.springframework.web.bind.annotation.RequestMethod;
|
||||
import org.springframework.web.bind.annotation.RequestParam;
|
||||
import org.springframework.web.bind.annotation.ResponseBody;
|
||||
|
||||
import javax.annotation.Resource;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
|
@@ -18,11 +18,7 @@
|
||||
package org.apache.rocketmq.dashboard.controller;
|
||||
|
||||
import com.google.common.collect.Maps;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import javax.annotation.Resource;
|
||||
|
||||
import jakarta.annotation.Resource;
|
||||
import org.apache.rocketmq.common.Pair;
|
||||
import org.apache.rocketmq.dashboard.model.MessageView;
|
||||
import org.apache.rocketmq.dashboard.model.trace.MessageTraceGraph;
|
||||
@@ -36,6 +32,9 @@ import org.springframework.web.bind.annotation.RequestMethod;
|
||||
import org.springframework.web.bind.annotation.RequestParam;
|
||||
import org.springframework.web.bind.annotation.ResponseBody;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
@Controller
|
||||
@RequestMapping("/messageTrace")
|
||||
@Permission
|
||||
|
@@ -16,7 +16,7 @@
|
||||
*/
|
||||
package org.apache.rocketmq.dashboard.controller;
|
||||
|
||||
import javax.annotation.Resource;
|
||||
import jakarta.annotation.Resource;
|
||||
import org.apache.rocketmq.dashboard.model.ConsumerMonitorConfig;
|
||||
import org.apache.rocketmq.dashboard.permisssion.Permission;
|
||||
import org.apache.rocketmq.dashboard.service.MonitorService;
|
||||
|
@@ -16,7 +16,7 @@
|
||||
*/
|
||||
package org.apache.rocketmq.dashboard.controller;
|
||||
|
||||
import javax.annotation.Resource;
|
||||
import jakarta.annotation.Resource;
|
||||
import org.apache.rocketmq.dashboard.aspect.admin.annotation.OriginalControllerReturnValue;
|
||||
import org.apache.rocketmq.dashboard.permisssion.Permission;
|
||||
import org.apache.rocketmq.dashboard.service.OpsService;
|
||||
|
@@ -17,7 +17,7 @@
|
||||
package org.apache.rocketmq.dashboard.controller;
|
||||
|
||||
import com.google.common.base.Preconditions;
|
||||
import javax.annotation.Resource;
|
||||
import jakarta.annotation.Resource;
|
||||
import org.apache.commons.lang3.StringUtils;
|
||||
import org.apache.rocketmq.dashboard.permisssion.Permission;
|
||||
import org.apache.rocketmq.dashboard.service.OpsService;
|
||||
|
@@ -16,11 +16,12 @@
|
||||
*/
|
||||
package org.apache.rocketmq.dashboard.controller;
|
||||
|
||||
import javax.annotation.Resource;
|
||||
import org.apache.rocketmq.remoting.protocol.body.ProducerConnection;
|
||||
|
||||
import jakarta.annotation.Resource;
|
||||
import org.apache.rocketmq.dashboard.model.ConnectionInfo;
|
||||
import org.apache.rocketmq.dashboard.permisssion.Permission;
|
||||
import org.apache.rocketmq.dashboard.service.ProducerService;
|
||||
import org.apache.rocketmq.remoting.protocol.body.ProducerConnection;
|
||||
import org.springframework.stereotype.Controller;
|
||||
import org.springframework.web.bind.annotation.RequestMapping;
|
||||
import org.springframework.web.bind.annotation.RequestMethod;
|
||||
|
@@ -16,6 +16,7 @@
|
||||
*/
|
||||
package org.apache.rocketmq.dashboard.controller;
|
||||
|
||||
import jakarta.annotation.Resource;
|
||||
import org.apache.rocketmq.dashboard.permisssion.Permission;
|
||||
import org.apache.rocketmq.dashboard.service.ProxyService;
|
||||
import org.springframework.stereotype.Controller;
|
||||
@@ -24,8 +25,6 @@ import org.springframework.web.bind.annotation.RequestMethod;
|
||||
import org.springframework.web.bind.annotation.RequestParam;
|
||||
import org.springframework.web.bind.annotation.ResponseBody;
|
||||
|
||||
import javax.annotation.Resource;
|
||||
|
||||
@Controller
|
||||
@RequestMapping("/proxy")
|
||||
@Permission
|
||||
@@ -38,6 +37,7 @@ public class ProxyController {
|
||||
return proxyService.getProxyHomePage();
|
||||
}
|
||||
|
||||
|
||||
@RequestMapping(value = "/addProxyAddr.do", method = RequestMethod.POST)
|
||||
@ResponseBody
|
||||
public Object addProxyAddr(@RequestParam String newProxyAddr) {
|
||||
|
@@ -16,6 +16,7 @@
|
||||
*/
|
||||
package org.apache.rocketmq.dashboard.controller;
|
||||
|
||||
import jakarta.annotation.Resource;
|
||||
import org.apache.rocketmq.client.consumer.DefaultMQPushConsumer;
|
||||
import org.apache.rocketmq.client.consumer.listener.ConsumeConcurrentlyContext;
|
||||
import org.apache.rocketmq.client.consumer.listener.ConsumeConcurrentlyStatus;
|
||||
@@ -26,11 +27,9 @@ import org.apache.rocketmq.client.producer.SendResult;
|
||||
import org.apache.rocketmq.common.consumer.ConsumeFromWhere;
|
||||
import org.apache.rocketmq.common.message.Message;
|
||||
import org.apache.rocketmq.common.message.MessageExt;
|
||||
import org.apache.rocketmq.remoting.exception.RemotingException;
|
||||
import java.util.List;
|
||||
import javax.annotation.Resource;
|
||||
import org.apache.rocketmq.dashboard.config.RMQConfigure;
|
||||
import org.apache.rocketmq.dashboard.util.JsonUtil;
|
||||
import org.apache.rocketmq.remoting.exception.RemotingException;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
import org.springframework.stereotype.Controller;
|
||||
@@ -38,6 +37,8 @@ import org.springframework.web.bind.annotation.RequestMapping;
|
||||
import org.springframework.web.bind.annotation.RequestMethod;
|
||||
import org.springframework.web.bind.annotation.ResponseBody;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
@Controller
|
||||
@RequestMapping("/test")
|
||||
public class TestController {
|
||||
|
@@ -16,16 +16,17 @@
|
||||
*/
|
||||
package org.apache.rocketmq.dashboard.controller;
|
||||
|
||||
import com.google.common.base.Preconditions;
|
||||
import jakarta.annotation.Resource;
|
||||
import org.apache.commons.collections.CollectionUtils;
|
||||
import org.apache.rocketmq.client.exception.MQClientException;
|
||||
import org.apache.rocketmq.dashboard.permisssion.Permission;
|
||||
import org.apache.rocketmq.remoting.exception.RemotingException;
|
||||
import org.apache.rocketmq.dashboard.model.request.SendTopicMessageRequest;
|
||||
import org.apache.rocketmq.dashboard.model.request.TopicConfigInfo;
|
||||
import org.apache.rocketmq.dashboard.permisssion.Permission;
|
||||
import org.apache.rocketmq.dashboard.service.ConsumerService;
|
||||
import org.apache.rocketmq.dashboard.service.TopicService;
|
||||
import org.apache.rocketmq.dashboard.util.JsonUtil;
|
||||
import com.google.common.base.Preconditions;
|
||||
import org.apache.commons.collections.CollectionUtils;
|
||||
import org.apache.rocketmq.remoting.exception.RemotingException;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
import org.springframework.stereotype.Controller;
|
||||
@@ -33,8 +34,6 @@ import org.springframework.web.bind.annotation.RequestBody;
|
||||
import org.springframework.web.bind.annotation.RequestMapping;
|
||||
import org.springframework.web.bind.annotation.RequestMethod;
|
||||
import org.springframework.web.bind.annotation.RequestParam;
|
||||
|
||||
import javax.annotation.Resource;
|
||||
import org.springframework.web.bind.annotation.ResponseBody;
|
||||
|
||||
@Controller
|
||||
@@ -56,6 +55,7 @@ public class TopicController {
|
||||
return topicService.fetchAllTopicList(skipSysProcess, skipRetryAndDlq);
|
||||
}
|
||||
|
||||
|
||||
@RequestMapping(value = "/list.queryTopicType", method = RequestMethod.GET)
|
||||
@ResponseBody
|
||||
public Object listTopicType() {
|
||||
|
@@ -17,27 +17,29 @@
|
||||
|
||||
package org.apache.rocketmq.dashboard.filter;
|
||||
|
||||
import jakarta.servlet.Filter;
|
||||
import jakarta.servlet.FilterChain;
|
||||
import jakarta.servlet.FilterConfig;
|
||||
import jakarta.servlet.ServletException;
|
||||
import jakarta.servlet.ServletRequest;
|
||||
import jakarta.servlet.ServletResponse;
|
||||
import jakarta.servlet.annotation.WebFilter;
|
||||
import jakarta.servlet.http.HttpServletResponse;
|
||||
|
||||
import java.io.IOException;
|
||||
import javax.servlet.Filter;
|
||||
import javax.servlet.FilterChain;
|
||||
import javax.servlet.FilterConfig;
|
||||
import javax.servlet.ServletException;
|
||||
import javax.servlet.ServletRequest;
|
||||
import javax.servlet.ServletResponse;
|
||||
import javax.servlet.annotation.WebFilter;
|
||||
import javax.servlet.http.HttpServletResponse;
|
||||
|
||||
|
||||
|
||||
@WebFilter(urlPatterns = "/*", filterName = "httpBasicAuthorizedFilter")
|
||||
public class HttpBasicAuthorizedFilter implements Filter {
|
||||
|
||||
|
||||
@Override
|
||||
public void init(FilterConfig config) throws ServletException {
|
||||
}
|
||||
|
||||
@Override
|
||||
public void doFilter(ServletRequest request, ServletResponse response,
|
||||
FilterChain chain) throws IOException, ServletException {
|
||||
FilterChain chain) throws IOException, ServletException {
|
||||
HttpServletResponse httpResponse = (HttpServletResponse) response;
|
||||
httpResponse.setCharacterEncoding("UTF-8");
|
||||
httpResponse.setContentType("application/json; charset=utf-8");
|
||||
|
@@ -17,22 +17,24 @@
|
||||
|
||||
package org.apache.rocketmq.dashboard.interceptor;
|
||||
|
||||
import jakarta.servlet.http.HttpServletRequest;
|
||||
import jakarta.servlet.http.HttpServletResponse;
|
||||
import org.apache.rocketmq.dashboard.service.LoginService;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.stereotype.Component;
|
||||
import org.springframework.web.servlet.handler.HandlerInterceptorAdapter;
|
||||
import org.springframework.web.servlet.HandlerInterceptor;
|
||||
|
||||
import javax.servlet.http.HttpServletRequest;
|
||||
import javax.servlet.http.HttpServletResponse;
|
||||
|
||||
@Component
|
||||
public class AuthInterceptor extends HandlerInterceptorAdapter {
|
||||
public class AuthInterceptor implements HandlerInterceptor {
|
||||
|
||||
@Autowired
|
||||
private LoginService loginService;
|
||||
|
||||
@Override
|
||||
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler)
|
||||
throws Exception {
|
||||
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
|
||||
return loginService.login(request, response);
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
@@ -17,11 +17,12 @@
|
||||
package org.apache.rocketmq.dashboard.model;
|
||||
|
||||
import com.google.common.collect.Sets;
|
||||
import java.util.Collection;
|
||||
import java.util.HashSet;
|
||||
import org.apache.rocketmq.common.MQVersion;
|
||||
import org.apache.rocketmq.remoting.protocol.body.Connection;
|
||||
|
||||
import java.util.Collection;
|
||||
import java.util.HashSet;
|
||||
|
||||
public class ConnectionInfo extends Connection {
|
||||
private String versionDesc;
|
||||
|
||||
|
@@ -16,8 +16,8 @@
|
||||
*/
|
||||
package org.apache.rocketmq.dashboard.model;
|
||||
|
||||
import org.apache.rocketmq.remoting.protocol.admin.RollbackStats;
|
||||
import com.google.common.collect.Lists;
|
||||
import org.apache.rocketmq.remoting.protocol.admin.RollbackStats;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
|
@@ -22,12 +22,13 @@ import com.alibaba.excel.annotation.write.style.ColumnWidth;
|
||||
import com.alibaba.excel.metadata.BaseRowModel;
|
||||
import com.alibaba.excel.util.DateUtils;
|
||||
import com.google.common.base.Charsets;
|
||||
import java.io.Serializable;
|
||||
import java.util.Date;
|
||||
import lombok.Data;
|
||||
import lombok.NoArgsConstructor;
|
||||
import org.apache.rocketmq.common.message.MessageExt;
|
||||
|
||||
import java.io.Serializable;
|
||||
import java.util.Date;
|
||||
|
||||
@Data
|
||||
@NoArgsConstructor
|
||||
public class DlqMessageExcelModel extends BaseRowModel implements Serializable {
|
||||
|
@@ -15,35 +15,19 @@
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
.view {
|
||||
}
|
||||
package org.apache.rocketmq.dashboard.model;
|
||||
|
||||
.view .ng-enter {
|
||||
overflow-y: auto;
|
||||
}
|
||||
import lombok.Getter;
|
||||
import lombok.Setter;
|
||||
|
||||
.view .ng-leave {
|
||||
opacity: 0;
|
||||
-webkit-transition: opacity .2s linear;
|
||||
transition: opacity 0.2s linear;
|
||||
}
|
||||
import java.util.List;
|
||||
|
||||
.ng-hide-add {
|
||||
transform: rotateZ(0);
|
||||
transform-origin: right;
|
||||
transition: all 0.2s ease-in-out;
|
||||
}
|
||||
@Setter
|
||||
@Getter
|
||||
public class Entry {
|
||||
private List<String> resource;
|
||||
private List<String> actions;
|
||||
private List<String> sourceIps;
|
||||
private String decision;
|
||||
|
||||
.ng-hide-add.ng-hide-add-active {
|
||||
transform: rotateZ(-135deg);
|
||||
}
|
||||
|
||||
.ng-hide-remove {
|
||||
transform: rotateY(90deg);
|
||||
transform-origin: left;
|
||||
transition: all 0.2s ease;
|
||||
}
|
||||
|
||||
.ng-hide-remove.ng-hide-remove-active {
|
||||
transform: rotateY(0);
|
||||
}
|
@@ -19,6 +19,7 @@ package org.apache.rocketmq.dashboard.model;
|
||||
import org.apache.rocketmq.remoting.protocol.heartbeat.ConsumeType;
|
||||
import org.apache.rocketmq.remoting.protocol.heartbeat.MessageModel;
|
||||
|
||||
import java.util.Date;
|
||||
import java.util.List;
|
||||
|
||||
public class GroupConsumeInfo implements Comparable<GroupConsumeInfo> {
|
||||
@@ -31,6 +32,7 @@ public class GroupConsumeInfo implements Comparable<GroupConsumeInfo> {
|
||||
private int consumeTps;
|
||||
private long diffTotal = -1;
|
||||
private String subGroupType = "NORMAL";
|
||||
private Date updateTime;
|
||||
|
||||
|
||||
public String getGroup() {
|
||||
@@ -112,4 +114,12 @@ public class GroupConsumeInfo implements Comparable<GroupConsumeInfo> {
|
||||
public void setVersion(String version) {
|
||||
this.version = version;
|
||||
}
|
||||
|
||||
public Date getUpdateTime() {
|
||||
return updateTime;
|
||||
}
|
||||
|
||||
public void setUpdateTime(Date updateTime) {
|
||||
this.updateTime = updateTime;
|
||||
}
|
||||
}
|
||||
|
@@ -16,8 +16,8 @@
|
||||
*/
|
||||
package org.apache.rocketmq.dashboard.model;
|
||||
|
||||
import org.apache.rocketmq.common.message.MessageExt;
|
||||
import com.google.common.base.Charsets;
|
||||
import org.apache.rocketmq.common.message.MessageExt;
|
||||
import org.springframework.beans.BeanUtils;
|
||||
|
||||
import java.net.SocketAddress;
|
||||
|
@@ -0,0 +1,30 @@
|
||||
/*
|
||||
* 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.model;
|
||||
|
||||
import lombok.Getter;
|
||||
import lombok.Setter;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
@Setter
|
||||
@Getter
|
||||
public class Policy {
|
||||
private String policyType;
|
||||
private List<Entry> entries;
|
||||
}
|
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user