This is an automated email from the ASF dual-hosted git repository.
jinrongtong pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/rocketmq-dashboard.git
The following commit(s) were added to refs/heads/master by this push:
new a4e02f4 [Enhancement] ACL can add rules in clusters (#340)
a4e02f4 is described below
commit a4e02f472fb666e8f149bd07ebbeff8c7e6629ba
Author: Crazylychee <[email protected]>
AuthorDate: Tue Jul 8 10:46:25 2025 +0800
[Enhancement] ACL can add rules in clusters (#340)
* [Enhancement] ACL can add rules in clusters and fix ISSUE #297
* rollback the yml change
---
docs/1_0_0/UserGuide_CN.md | 5 +-
frontend-new/src/api/remoteApi/remoteApi.js | 42 +-
frontend-new/src/components/acl/SubjectInput.jsx | 18 +-
.../src/components/consumer/ClientInfoModal.jsx | 117 ++++-
.../components/consumer/ConsumerDetailModal.jsx | 88 +++-
frontend-new/src/i18n/index.js | 35 ++
frontend-new/src/pages/Acl/acl.jsx | 548 ++++++++++-----------
frontend-new/src/pages/Consumer/consumer.jsx | 36 +-
frontend-new/src/pages/Message/message.jsx | 3 +-
.../dashboard/controller/AclController.java | 31 +-
.../apache/rocketmq/dashboard/model/AclInfo.java | 209 ++++++++
.../rocketmq/dashboard/model/PolicyRequest.java | 3 +-
.../model/{PolicyRequest.java => UserInfoDto.java} | 32 +-
.../dashboard/model/request/UserCreateRequest.java | 3 +-
.../dashboard/model/request/UserUpdateRequest.java | 3 +-
.../rocketmq/dashboard/service/AclService.java | 17 +-
.../dashboard/service/impl/AclServiceImpl.java | 291 +++++++----
.../service/impl/ConsumerServiceImpl.java | 25 +-
.../service/provider/UserInfoProviderImpl.java | 5 +-
.../dashboard/controller/AclControllerTest.java | 223 +++++----
20 files changed, 1124 insertions(+), 610 deletions(-)
diff --git a/docs/1_0_0/UserGuide_CN.md b/docs/1_0_0/UserGuide_CN.md
index 49de4e3..25afa53 100755
--- a/docs/1_0_0/UserGuide_CN.md
+++ b/docs/1_0_0/UserGuide_CN.md
@@ -94,8 +94,9 @@
## ACL2.0管理界面
-- 支持根据broker地址的acl规则的查询
+- 支持根据集群名字或者broker地址的acl规则的查询
- acl规则的修改、新增、删除、查找
+- 如果只是选取了集群名字,那么查询的acl列表将会取交集,如果选取了brokerName,就会返回该broker的acl列表。
- (不再支持acl1.0)

@@ -188,4 +189,4 @@ rolePerms:
- /monitor/*
....
```
-*
3.前端页面显示上,为了更好区分普通用户和admin用户权限,关于资源的删除、更新等操作按钮不对普通用户角色显示,如果要执行资源相关操作,需要退出使用admin角色登录。
\ No newline at end of file
+*
3.前端页面显示上,为了更好区分普通用户和admin用户权限,关于资源的删除、更新等操作按钮不对普通用户角色显示,如果要执行资源相关操作,需要退出使用admin角色登录。
diff --git a/frontend-new/src/api/remoteApi/remoteApi.js
b/frontend-new/src/api/remoteApi/remoteApi.js
index af75920..d2390f5 100644
--- a/frontend-new/src/api/remoteApi/remoteApi.js
+++ b/frontend-new/src/api/remoteApi/remoteApi.js
@@ -15,7 +15,7 @@
* limitations under the License.
*/
const appConfig = {
- apiBaseUrl: 'http://localhost:8082' // 请替换为你的实际 API Base URL
+ apiBaseUrl: 'http://localhost:8082'
};
let _redirectHandler = null;
@@ -74,34 +74,36 @@ const remoteApi = {
}
},
- listUsers: async (brokerAddress) => {
+ listUsers: async (brokerName, clusterName) => {
const params = new URLSearchParams();
- if (brokerAddress) params.append('brokerAddress', brokerAddress);
+ if (brokerName) params.append('brokerName', brokerName);
+ if (clusterName) params.append('clusterName', clusterName);
const response = await
remoteApi._fetch(remoteApi.buildUrl(`/acl/users.query?${params.toString()}`));
return await response.json();
},
- createUser: async (brokerAddress, userInfo) => {
+ createUser: async (brokerName, userInfo, clusterName) => {
const response = await
remoteApi._fetch(remoteApi.buildUrl('/acl/createUser.do'), {
method: 'POST',
headers: {'Content-Type': 'application/json'},
- body: JSON.stringify({brokerAddress, userInfo})
+ body: JSON.stringify({brokerName, userInfo, clusterName})
});
- return await response.json(); // 返回字符串消息
+ return await response.json();
},
- updateUser: async (brokerAddress, userInfo) => {
+ updateUser: async (brokerName, userInfo, clusterName) => {
const response = await
remoteApi._fetch(remoteApi.buildUrl('/acl/updateUser.do'), {
method: 'POST',
headers: {'Content-Type': 'application/json'},
- body: JSON.stringify({brokerAddress, userInfo})
+ body: JSON.stringify({brokerName, userInfo, clusterName})
});
return await response.json();
},
- deleteUser: async (brokerAddress, username) => {
+ deleteUser: async (brokerName, username, clusterName) => {
const params = new URLSearchParams();
- if (brokerAddress) params.append('brokerAddress', brokerAddress);
+ if (brokerName) params.append('brokerName', brokerName);
+ if (clusterName) params.append('clusterName', clusterName);
params.append('username', username);
const response = await
remoteApi._fetch(remoteApi.buildUrl(`/acl/deleteUser.do?${params.toString()}`),
{
method: 'DELETE'
@@ -109,38 +111,40 @@ const remoteApi = {
return await response.json();
},
- // --- ACL 权限相关 API ---
- listAcls: async (brokerAddress, searchParam) => {
+ listAcls: async (brokerName, searchParam, clusterName) => {
const params = new URLSearchParams();
- if (brokerAddress) params.append('brokerAddress', brokerAddress);
+ if (brokerName) params.append('brokerName', brokerName);
+ if (clusterName) params.append('clusterName', clusterName);
if (searchParam) params.append('searchParam', searchParam);
+ if (searchParam != null) console.log(1111)
const response = await
remoteApi._fetch(remoteApi.buildUrl(`/acl/acls.query?${params.toString()}`));
return await response.json();
},
- createAcl: async (brokerAddress, subject, policies) => {
+ createAcl: async (brokerName, subject, policies, clusterName) => {
const response = await
remoteApi._fetch(remoteApi.buildUrl('/acl/createAcl.do'), {
method: 'POST',
headers: {'Content-Type': 'application/json'},
- body: JSON.stringify({brokerAddress, subject, policies})
+ body: JSON.stringify({brokerName, subject, policies, clusterName})
});
return await response.json();
},
- updateAcl: async (brokerAddress, subject, policies) => {
+ updateAcl: async (brokerName, subject, policies, clusterName) => {
const response = await
remoteApi._fetch(remoteApi.buildUrl('/acl/updateAcl.do'), {
method: 'POST',
headers: {'Content-Type': 'application/json'},
- body: JSON.stringify({brokerAddress, subject, policies})
+ body: JSON.stringify({brokerName, subject, policies, clusterName})
});
return await response.json();
},
- deleteAcl: async (brokerAddress, subject, resource) => {
+ deleteAcl: async (brokerName, subject, resource, clusterName) => {
const params = new URLSearchParams();
- if (brokerAddress) params.append('brokerAddress', brokerAddress);
+ if (brokerName) params.append('brokerAddress', brokerName);
params.append('subject', subject);
if (resource) params.append('resource', resource);
+ if (clusterName) params.append('clusterName', clusterName);
const response = await
remoteApi._fetch(remoteApi.buildUrl(`/acl/deleteAcl.do?${params.toString()}`), {
method: 'DELETE'
});
diff --git a/frontend-new/src/components/acl/SubjectInput.jsx
b/frontend-new/src/components/acl/SubjectInput.jsx
index 4c2ee61..a3b6dc7 100644
--- a/frontend-new/src/components/acl/SubjectInput.jsx
+++ b/frontend-new/src/components/acl/SubjectInput.jsx
@@ -20,13 +20,13 @@ import React, {useEffect, useState} from 'react';
const {Option} = Select;
-// Subject 类型枚举
+
const subjectTypes = [
{value: 'User', label: 'User'},
];
-const SubjectInput = ({value, onChange, disabled}) => {
- // 解析传入的 value,将其拆分为 type 和 name
+const SubjectInput = ({value, onChange, disabled, t}) => {
+
const parseValue = (val) => {
if (!val || typeof val !== 'string') {
return {type: subjectTypes[0].value, name: ''}; // 默认值
@@ -35,27 +35,25 @@ const SubjectInput = ({value, onChange, disabled}) => {
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,类型设为默认
+ return {type: subjectTypes[0].value, name: val};
};
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 if (changedType) {
+ onChange('');
} else {
onChange('');
}
@@ -91,7 +89,7 @@ const SubjectInput = ({value, onChange, disabled}) => {
style={{width: '70%'}}
value={currentName}
onChange={onNameChange}
- placeholder="请输入名称 (例如: yourUsername)"
+ placeholder={t.PLEASE_INPUT_NAME}
disabled={disabled}
/>
</Input.Group>
diff --git a/frontend-new/src/components/consumer/ClientInfoModal.jsx
b/frontend-new/src/components/consumer/ClientInfoModal.jsx
index 2821cbf..4cf5de5 100644
--- a/frontend-new/src/components/consumer/ClientInfoModal.jsx
+++ b/frontend-new/src/components/consumer/ClientInfoModal.jsx
@@ -16,15 +16,15 @@
*/
import React, {useEffect, useState} from 'react';
-import {Modal, Spin, Table} from 'antd';
+import {Descriptions, Modal, Spin, Table, Tag, Tooltip} 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 () => {
@@ -33,10 +33,8 @@ const ClientInfoModal = ({visible, group, address,
onCancel}) => {
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);
}
@@ -46,53 +44,118 @@ const ClientInfoModal = ({visible, group, address,
onCancel}) => {
}, [visible, group, address]);
const connectionColumns = [
- {title: 'ClientId', dataIndex: 'clientId'},
- {title: 'ClientAddr', dataIndex: 'clientAddr'},
- {title: 'Language', dataIndex: 'language'},
- {title: 'Version', dataIndex: 'versionDesc'},
+ {
+ title: t.CLIENTID, dataIndex: 'clientId', key: 'clientId', width:
220, ellipsis: true,
+ render: (text) => (
+ <Tooltip title={text}>
+ {text}
+ </Tooltip>
+ )
+ },
+ {title: t.CLIENTADDR, dataIndex: 'clientAddr', key: 'clientAddr',
width: 150, ellipsis: true},
+ {title: t.LANGUAGE, dataIndex: 'language', key: 'language', width:
100},
+ {title: t.VERSION, dataIndex: 'versionDesc', key: 'versionDesc',
width: 100},
];
const subscriptionColumns = [
- {title: 'Topic', dataIndex: 'topic'},
- {title: 'SubExpression', dataIndex: 'subString'},
+ {
+ title: t.TOPIC, dataIndex: 'topic', key: 'topic', width: 250,
ellipsis: true,
+ render: (text) => (
+ <Tooltip title={text}>
+ {text}
+ </Tooltip>
+ )
+ },
+ {title: t.SUBSCRIPTION_EXPRESSION, dataIndex: 'subString', key:
'subString', width: 150, ellipsis: true},
+ {
+ title: t.EXPRESSION_TYPE, dataIndex: 'expressionType', key:
'expressionType', width: 120,
+ render: (text) => <Tag color="blue">{text}</Tag>
+ },
+ // --- Added Columns for TagsSet and CodeSet ---
+ {
+ title: t.TAGS_SET, // Ensure t.TAGS_SET is defined in your
language file
+ dataIndex: 'tagsSet',
+ key: 'tagsSet',
+ width: 150,
+ render: (tags) => (
+ tags && tags.length > 0 ? (
+ <Tooltip title={tags.join(', ')}>
+ {tags.map((tag, index) => (
+ <Tag key={index} color="default">{tag}</Tag>
+ ))}
+ </Tooltip>
+ ) : 'N/A'
+ ),
+ ellipsis: true,
+ },
+ {
+ title: t.CODE_SET, // Ensure t.CODE_SET is defined in your
language file
+ dataIndex: 'codeSet',
+ key: 'codeSet',
+ width: 150,
+ render: (codes) => (
+ codes && codes.length > 0 ? (
+ <Tooltip title={codes.join(', ')}>
+ {codes.map((code, index) => (
+ <Tag key={index} color="default">{code}</Tag>
+ ))}
+ </Tooltip>
+ ) : 'N/A'
+ ),
+ ellipsis: true,
+ },
+ // --- End of Added Columns ---
+ {title: t.SUB_VERSION, dataIndex: 'subVersion', key: 'subVersion',
width: 150},
];
+ const formattedSubscriptionData = connectionData?.subscriptionTable
+ ? Object.keys(connectionData.subscriptionTable).map(key => ({
+ ...connectionData.subscriptionTable[key],
+ key: key,
+ }))
+ : [];
+
return (
<Modal
- title={`[${group}]${t.CLIENT}`}
+ title={`[${group}] ${t.CLIENT_INFORMATION}`}
visible={visible}
onCancel={onCancel}
footer={null}
- width={800}
+ width={1200} // Increased width to accommodate more columns
>
<Spin spinning={loading}>
{connectionData && (
<>
+ <Descriptions bordered column={2}
title={t.CONNECTION_OVERVIEW} style={{marginBottom: 20}}>
+ <Descriptions.Item label={t.CONSUME_TYPE}>
+ <Tag
color="green">{connectionData.consumeType}</Tag>
+ </Descriptions.Item>
+ <Descriptions.Item label={t.MESSAGE_MODEL}>
+ <Tag
color="geekblue">{connectionData.messageModel}</Tag>
+ </Descriptions.Item>
+ <Descriptions.Item label={t.CONSUME_FROM_WHERE}>
+ <Tag
color="purple">{connectionData.consumeFromWhere}</Tag>
+ </Descriptions.Item>
+ </Descriptions>
+
+ <h3>{t.CLIENT_CONNECTIONS}</h3>
<Table
columns={connectionColumns}
dataSource={connectionData.connectionSet}
rowKey="clientId"
pagination={false}
+ scroll={{x: 'max-content'}}
+ style={{marginBottom: 20}}
/>
- <h4>{t.SUBSCRIPTION}</h4>
+
+ <h3>{t.CLIENT_SUBSCRIPTIONS}</h3>
<Table
columns={subscriptionColumns}
- dataSource={
- subscriptionData?.subscriptionTable
- ?
Object.entries(subscriptionData.subscriptionTable).map(([topic, detail]) => ({
- topic,
- ...detail,
- }))
- : []
- }
- rowKey="topic"
+ dataSource={formattedSubscriptionData}
+ rowKey="key"
pagination={false}
- locale={{
- emptyText: loading ? <Spin size="small"/> :
t.NO_DATA
- }}
+ scroll={{x: 'max-content'}}
/>
- <p>ConsumeType: {connectionData.consumeType}</p>
- <p>MessageModel: {connectionData.messageModel}</p>
</>
)}
</Spin>
diff --git a/frontend-new/src/components/consumer/ConsumerDetailModal.jsx
b/frontend-new/src/components/consumer/ConsumerDetailModal.jsx
index 4c89acd..b46ffe7 100644
--- a/frontend-new/src/components/consumer/ConsumerDetailModal.jsx
+++ b/frontend-new/src/components/consumer/ConsumerDetailModal.jsx
@@ -43,32 +43,88 @@ const ConsumerDetailModal = ({visible, group, address,
onCancel}) => {
fetchData();
}, [visible, group, address]);
+ // Format timestamp to readable date
+ const formatTimestamp = (timestamp) => {
+ if (!timestamp || timestamp === 0) return '-';
+ return new Date(timestamp).toLocaleString();
+ };
+
+ // Group data by topic for better organization
+ const groupByTopic = (data) => {
+ const grouped = {};
+ data.forEach(item => {
+ if (!grouped[item.topic]) {
+ grouped[item.topic] = [];
+ }
+ grouped[item.topic].push(item);
+ });
+ return grouped;
+ };
+
+ const groupedDetails = groupByTopic(details);
+
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'},
+ {title: 'Broker', dataIndex: 'brokerName', width: 120},
+ {title: 'Queue ID', dataIndex: 'queueId', width: 100},
+ {title: 'Broker Offset', dataIndex: 'brokerOffset', width: 120},
+ {title: 'Consumer Offset', dataIndex: 'consumerOffset', width: 120},
+ {
+ title: 'Lag (Diff)', dataIndex: 'diffTotal', width: 100,
+ render: (diff) => (
+ <span style={{color: diff > 0 ? '#f5222d' : '#52c41a'}}>
+ {diff}
+ </span>
+ )
+ },
+ {title: 'Client Info', dataIndex: 'clientInfo', width: 200},
+ {
+ title: 'Last Consume Time', dataIndex: 'lastTimestamp', width: 180,
+ render: (timestamp) => formatTimestamp(timestamp)
+ },
];
return (
<Modal
- title={`[${group}]${t.CONSUME_DETAIL}`}
+ title={
+ <span>Consumer Details - Group: <strong>{group}</strong> |
Address: <strong>{address}</strong></span>}
visible={visible}
onCancel={onCancel}
footer={null}
- width={1200}
+ width={1400}
+ style={{top: 20}}
>
<Spin spinning={loading}>
- {details.map((consumeDetail, index) => (
- <div key={index}>
- <Table
- columns={queueColumns}
- dataSource={consumeDetail.queueStatInfoList}
- rowKey="queueId"
- pagination={false}
- />
+ {Object.entries(groupedDetails).map(([topic, topicDetails]) =>
(
+ <div key={topic} style={{marginBottom: 24}}>
+ <div style={{
+ background: '#f0f0f0',
+ padding: '8px 16px',
+ marginBottom: 8,
+ borderRadius: 4,
+ display: 'flex',
+ justifyContent: 'space-between',
+ alignItems: 'center'
+ }}>
+ <h3 style={{margin: 0}}>Topic:
<strong>{topic}</strong></h3>
+ <div>
+ <span style={{marginRight: 16}}>Total Lag:
<strong>{topicDetails[0].diffTotal}</strong></span>
+ <span>Last Consume Time:
<strong>{formatTimestamp(topicDetails[0].lastTimestamp)}</strong></span>
+ </div>
+ </div>
+
+ {topicDetails.map((detail, index) => (
+ <div key={index} style={{marginBottom: 16}}>
+ <Table
+ columns={queueColumns}
+ dataSource={detail.queueStatInfoList}
+ rowKey={(record) =>
`${record.brokerName}-${record.queueId}`}
+ pagination={false}
+ size="small"
+ bordered
+ scroll={{x: 'max-content'}}
+ />
+ </div>
+ ))}
</div>
))}
</Spin>
diff --git a/frontend-new/src/i18n/index.js b/frontend-new/src/i18n/index.js
index 0e6bce1..1486991 100644
--- a/frontend-new/src/i18n/index.js
+++ b/frontend-new/src/i18n/index.js
@@ -290,6 +290,23 @@ export const translations = {
"USERNAME_PLACEHOLDER": "用户名",
"PASSWORD_REQUIRED": "密码为必填项",
"PASSWORD_PLACEHOLDER": "密码",
+ "PLEASE_INPUT_NAME":"请输入名称",
+ "PLEASE_SELECT_CLUSTER": "请选择集群",
+ "CLIENT_INFORMATION": "客户端信息",
+ "CONSUME_TYPE": "消费类型",
+ "MESSAGE_MODEL": "消息模型",
+ "CONSUME_FROM_WHERE": "从何处消费",
+ "CLIENT_CONNECTIONS": "客户端连接",
+ "CLIENT_SUBSCRIPTIONS": "客户端订阅",
+ "CONNECTION_OVERVIEW": "连接概览",
+ "CLIENTID": "客户端 ID",
+ "CLIENTADDR": "客户端地址",
+ "LANGUAGE": "语言",
+ "SUBSCRIPTION_EXPRESSION": "订阅表达式",
+ "EXPRESSION_TYPE": "表达式类型",
+ "SUB_VERSION": "订阅版本",
+ "CODE_SET": "代码集",
+ "TAGS_SET": "标签集"
},
en: {
"DEFAULT": "Default",
@@ -558,6 +575,24 @@ export const translations = {
"USERNAME_PLACEHOLDER": "Username placeholder",
"PASSWORD_REQUIRED": "Password is required",
"PASSWORD_PLACEHOLDER": "Password placeholder",
+ "PLEASE_INPUT_NAME": "Please input name",
+ "PLEASE_SELECT_CLUSTER": "Please select cluster",
+ "SUBSCRIPTION": "Subscription",
+ "CLIENT_INFORMATION": "Client Information",
+ "CONSUME_TYPE": "Consume Type",
+ "MESSAGE_MODEL": "Message Model",
+ "CONSUME_FROM_WHERE": "Consume From Where",
+ "CLIENT_CONNECTIONS": "Client Connections",
+ "CLIENT_SUBSCRIPTIONS": "Client Subscriptions",
+ "CONNECTION_OVERVIEW": "Connection Overview",
+ "CLIENTID": "Client ID",
+ "CLIENTADDR": "Client Address",
+ "LANGUAGE": "Language",
+ "SUBSCRIPTION_EXPRESSION": "Subscription Expression",
+ "EXPRESSION_TYPE": "Expression Type",
+ "SUB_VERSION": "Sub Version",
+ "CODE_SET": "Code Set",
+ "TAGS_SET": "Tags Set"
}
diff --git a/frontend-new/src/pages/Acl/acl.jsx
b/frontend-new/src/pages/Acl/acl.jsx
index aeab68d..c3652eb 100644
--- a/frontend-new/src/pages/Acl/acl.jsx
+++ b/frontend-new/src/pages/Acl/acl.jsx
@@ -15,32 +15,16 @@
* 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 React, {useEffect, useState} from 'react';
+import {Button, Form, Input, message, Modal, Popconfirm, Select, Space, Table,
Tabs, Tag} from 'antd';
+import {DeleteOutlined, EditOutlined, EyeInvisibleOutlined, EyeOutlined} 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 {TabPane} = Tabs;
+const {Search} = Input;
const Acl = () => {
const [activeTab, setActiveTab] = useState('users');
@@ -84,17 +68,19 @@ const Acl = () => {
// State for the address of the selected broker
const [brokerAddress, setBrokerAddress] = useState(undefined);
+ const [searchValue, setSearchValue] = useState('');
+
// --- Data Fetching and Initial Setup ---
useEffect(() => {
const fetchData = async () => {
const clusterResponse = await remoteApi.getClusterList();
if (clusterResponse.status === 0 && clusterResponse.data) {
- const { clusterInfo } = 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 })));
+ setClusterNamesOptions(clusterNames.map(name => ({label: name,
value: name})));
// Set initial selections if clusters are available
if (clusterNames.length > 0) {
@@ -119,15 +105,15 @@ const Acl = () => {
console.error('Failed to fetch cluster list:',
clusterResponse.errMsg);
}
};
- if(!clusterData){
- fetchData();
+ if (!clusterData) {
+ setLoading(true);
+ fetchData().finally(() => setLoading(false));
}
- if(brokerAddress){
- // Call fetchUsers or fetchAcls based on activeTab initially
+ if (brokerAddress) {
if (activeTab === 'users') {
- fetchUsers();
+ fetchUsers().finally(() => setLoading(false));
} else {
- fetchAcls();
+ fetchAcls().finally(() => setLoading(false));
}
}
@@ -135,7 +121,6 @@ const Acl = () => {
useEffect(() => {
const userPermission = localStorage.getItem('userrole');
- console.log(userPermission);
if (userPermission == 2) {
setWriteOperationEnabled(false);
} else {
@@ -150,7 +135,7 @@ const Acl = () => {
return;
}
const brokersInCluster = info.clusterAddrTable[clusterName] || [];
- setBrokerNamesOptions(brokersInCluster.map(broker => ({ label: broker,
value: broker })));
+ setBrokerNamesOptions(brokersInCluster.map(broker => ({label: broker,
value: broker})));
};
// --- Event Handlers ---
@@ -174,12 +159,6 @@ const Acl = () => {
}
};
- // --- 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));
@@ -197,7 +176,7 @@ const Acl = () => {
}
const invalidIps = value.filter(ip => !ipRegex.test(ip));
if (invalidIps.length > 0) {
- return Promise.reject(t.INVALID_IP_ADDRESSES +"ips:" +
invalidIps.join(', '));
+ return Promise.reject(t.INVALID_IP_ADDRESSES + "ips:" +
invalidIps.join(', '));
}
return Promise.resolve();
};
@@ -206,7 +185,7 @@ const Acl = () => {
const fetchUsers = async () => {
setLoading(true);
try {
- const result = await remoteApi.listUsers(brokerAddress);
+ const result = await remoteApi.listUsers(selectedBroker,
selectedCluster);
if (result && result.status === 0 && result.data) {
const formattedUsers = result.data.map(user => ({
...user,
@@ -215,7 +194,7 @@ const Acl = () => {
}));
setUserListData(formattedUsers);
} else {
- messageApi.error(t.GET_USERS_FAILED+result?.errMsg);
+ messageApi.error(t.GET_USERS_FAILED + result?.errMsg);
}
} catch (error) {
console.error("Failed to fetch users:", error);
@@ -225,10 +204,10 @@ const Acl = () => {
}
};
- const fetchAcls = async (value) => {
+ const fetchAcls = async () => {
setLoading(true);
try {
- const result = await remoteApi.listAcls(brokerAddress, value);
+ const result = await remoteApi.listAcls(selectedBroker,
searchValue, selectedCluster);
if (result && result.status === 0) {
const formattedAcls = [];
@@ -245,7 +224,6 @@ const Acl = () => {
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,
@@ -301,10 +279,10 @@ const Acl = () => {
const handleDeleteUser = async (username) => {
setLoading(true);
try {
- const result = await remoteApi.deleteUser(brokerAddress, username);
+ const result = await remoteApi.deleteUser(selectedBroker,
username, selectedCluster);
if (result.status === 0) {
messageApi.success(t.USER_DELETE_SUCCESS);
- fetchUsers(brokerAddress);
+ fetchUsers();
} else {
messageApi.error(t.USER_DELETE_FAILED + result.errMsg);
}
@@ -330,14 +308,14 @@ const Acl = () => {
};
if (currentUser) {
- result = await remoteApi.updateUser(brokerAddress,
userInfoParam);
+ result = await remoteApi.updateUser(selectedBroker,
userInfoParam, selectedCluster);
if (result.status === 0) {
messageApi.success(t.USER_UPDATE_SUCCESS);
} else {
messageApi.error(result.errMsg);
}
} else {
- result = await remoteApi.createUser(brokerAddress,
userInfoParam);
+ result = await remoteApi.createUser(selectedBroker,
userInfoParam, selectedCluster);
if (result.status === 0) {
messageApi.success(t.USER_CREATE_SUCCESS);
} else {
@@ -379,12 +357,12 @@ const Acl = () => {
const handleDeleteAcl = async (subject, resource) => {
setLoading(true);
try {
- const result = await remoteApi.deleteAcl(brokerAddress, subject,
resource);
+ const result = await remoteApi.deleteAcl(selectedBroker, subject,
resource, selectedCluster);
if (result.status === 0) {
messageApi.success(t.ACL_DELETE_SUCCESS);
fetchAcls();
} else {
- messageApi.error(t.ACL_DELETE_FAILED+result.errMsg);
+ messageApi.error(t.ACL_DELETE_FAILED + result.errMsg);
}
} catch (error) {
console.error("Failed to delete ACL:", error);
@@ -415,24 +393,23 @@ const Acl = () => {
];
if (isUpdate) { // This condition seems reversed for update/create
based on the current logic.
- result = await remoteApi.updateAcl(brokerAddress,
values.subject, policiesParam);
+ result = await remoteApi.updateAcl(selectedBroker,
values.subject, policiesParam, selectedCluster);
if (result.status === 0) {
messageApi.success(t.ACL_UPDATE_SUCCESS);
setIsAclModalVisible(false);
- fetchAcls(brokerAddress);
+ fetchAcls();
} else {
- messageApi.error(t.ACL_UPDATE_FAILED+result.errMsg);
+ messageApi.error(t.ACL_UPDATE_FAILED + result.errMsg);
}
setIsUpdate(false)
} else {
- result = await remoteApi.createAcl(brokerAddress,
values.subject, policiesParam);
- console.log(result)
+ result = await remoteApi.createAcl(selectedBroker,
values.subject, policiesParam, selectedCluster);
if (result.status === 0) {
messageApi.success(t.ACL_CREATE_SUCCESS);
setIsAclModalVisible(false);
- fetchAcls(brokerAddress);
+ fetchAcls();
} else {
- messageApi.error(t.ACL_CREATE_FAILED+result.errMsg);
+ messageApi.error(t.ACL_CREATE_FAILED + result.errMsg);
}
}
@@ -444,6 +421,10 @@ const Acl = () => {
}
};
+ const handleInputChange = (e) => {
+ setSearchValue(e.target.value);
+ };
+
// --- Search Functionality ---
const handleSearch = (value) => {
@@ -459,7 +440,7 @@ const Acl = () => {
setUserListData(filteredData);
}
} else {
- fetchAcls(value);
+ fetchAcls();
}
};
@@ -480,9 +461,9 @@ const Acl = () => {
{showPassword ? text : '********'}
<Button
type="link"
- icon={showPassword ? <EyeInvisibleOutlined /> :
<EyeOutlined />}
+ icon={showPassword ? <EyeInvisibleOutlined/> :
<EyeOutlined/>}
onClick={() => setShowPassword(!showPassword)}
- style={{ marginLeft: 8 }}
+ style={{marginLeft: 8}}
>
{showPassword ? t.HIDE : t.VIEW}
</Button>
@@ -508,14 +489,14 @@ const Acl = () => {
render: (_, record) => (
writeOperationEnabled ? (
<Space size="middle">
- <Button icon={<EditOutlined />} onClick={() =>
handleEditUser(record)}>{t.MODIFY}</Button>
+ <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>
+ <Button icon={<DeleteOutlined/>}
danger>{t.DELETE}</Button>
</Popconfirm>
</Space>
) : null
@@ -567,14 +548,14 @@ const Acl = () => {
render: (_, record) => (
writeOperationEnabled ? (
<Space size="middle">
- <Button icon={<EditOutlined />} onClick={() =>
handleEditAcl(record)}>{t.MODIFY}</Button>
+ <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>
+ <Button icon={<DeleteOutlined/>}
danger>{t.DELETE}</Button>
</Popconfirm>
</Space>
) : null
@@ -582,233 +563,236 @@ const Acl = () => {
},
];
-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>
+ return (
+ <>
+ {msgContextHolder}
+ <div style={{padding: 24}}>
+ <h2>{t.ACL_MANAGEMENT}</h2>
- <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/>)}
+ <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
- 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 label={t.PLEASE_SELECT_BROKER}
style={{marginBottom: 0}}>
+ <Select
+ placeholder={t.PLEASE_SELECT_BROKER}
+ style={{width: 200}}
+ onChange={handleBrokerChange}
+ value={selectedBroker}
+ options={brokerNamesOptions}
+ disabled={!selectedCluster}
+ allowClear
+ />
</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"
+ <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}
+ value={searchValue}
+ onChange={handleInputChange}
+ 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.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}]}
+ <Form
+ form={userForm}
+ layout="vertical"
+ name="user_form"
+ initialValues={{userStatus: 'enable'}}
>
- <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}]}
+ <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"
>
- {isUpdate ? (
- <Input disabled={isUpdate}/>
- ) : (
- <ResourceInput/>
- )}
- </Form.Item>
+ <Form.Item
+ name="subject"
+ label={t.SUBJECT_LABEL}
+ rules={[{required: true, message:
t.PLEASE_ENTER_SUBJECT}]}
+ >
+ <SubjectInput disabled={!!currentAcl} t={t}/>
+ </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={[',', ' ']}
+ <Form.Item
+ name="policyType"
+ label={t.POLICY_TYPE}
+ rules={[{required: true, message:
t.PLEASE_ENTER_POLICY_TYPE}]}
>
- <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>
- </>
-);
+ <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;
diff --git a/frontend-new/src/pages/Consumer/consumer.jsx
b/frontend-new/src/pages/Consumer/consumer.jsx
index fb3da7b..650bafa 100644
--- a/frontend-new/src/pages/Consumer/consumer.jsx
+++ b/frontend-new/src/pages/Consumer/consumer.jsx
@@ -66,7 +66,7 @@ const ConsumerGroupList = () => {
}
});
- const [proxyOptions ,setProxyOptions]= useState([]);
+ const [proxyOptions, setProxyOptions] = useState([]);
const [paginationConf, setPaginationConf] = useState({
current: 1,
pageSize: 10,
@@ -82,9 +82,9 @@ const ConsumerGroupList = () => {
setLoading(true);
try {
var response;
- if(!proxyEnabled){
+ if (!proxyEnabled) {
response = await remoteApi.queryConsumerGroupList(false);
- }else{
+ } else {
response = await remoteApi.queryConsumerGroupList(false,
selectedProxy);
}
if (response.status === 0) {
@@ -98,12 +98,12 @@ const ConsumerGroupList = () => {
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);
+ messageApi.error({title: t.ERROR, content:
t.FAILED_TO_FETCH_DATA});
} finally {
setLoading(false);
}
- }, [t]);
+ }, [t, proxyEnabled, selectedProxy, messageApi, setAllConsumerGroupList,
remoteApi, setLoading]);
const filterByType = (str, type, version) => {
if (filterSystem && type === "SYSTEM") return true;
@@ -465,15 +465,21 @@ const ConsumerGroupList = () => {
<>
{msgContextHolder}
{notificationContextHolder}
- <div style={{ padding: '20px' }}>
+ <div style={{padding: '20px'}}>
<Spin spinning={loading} tip={t.LOADING}>
- <div style={{ marginBottom: '20px', display: 'flex',
justifyContent: 'space-between', alignItems: 'center' }}>
+ <div style={{
+ marginBottom: '20px',
+ display: 'flex',
+ justifyContent: 'space-between',
+ alignItems: 'center'
+ }}>
{/* 左侧:筛选和操作按钮 */}
- <div style={{ display: 'flex', alignItems: 'center',
gap: '15px', flexWrap: 'wrap' }}>
- <div style={{ display: 'flex', alignItems:
'center' }}>
- <label style={{ marginRight: '8px',
whiteSpace: 'nowrap' }}>{t.SUBSCRIPTION_GROUP}:</label>
+ <div style={{display: 'flex', alignItems: 'center',
gap: '15px', flexWrap: 'wrap'}}>
+ <div style={{display: 'flex', alignItems:
'center'}}>
+ <label
+ style={{marginRight: '8px', whiteSpace:
'nowrap'}}>{t.SUBSCRIPTION_GROUP}:</label>
<Input
- style={{ width: '200px' }}
+ style={{width: '200px'}}
value={filterStr}
onChange={(e) =>
handleFilterInputChange(e.target.value)}
placeholder="输入订阅组名称"
@@ -504,10 +510,10 @@ const ConsumerGroupList = () => {
</div>
{/* 右侧:代理选项 */}
- <div style={{ display: 'flex', alignItems: 'center',
gap: '15px' }}>
- <label style={{ marginRight: '8px', whiteSpace:
'nowrap' }}>{t.SELECT_PROXY}:</label>
+ <div style={{display: 'flex', alignItems: 'center',
gap: '15px'}}>
+ <label style={{marginRight: '8px', whiteSpace:
'nowrap'}}>{t.SELECT_PROXY}:</label>
<Select
- style={{ width: '220px' }}
+ style={{width: '220px'}}
placeholder={t.SELECT_PROXY}
onChange={(value) => setSelectedProxy(value)}
value={selectedProxy}
@@ -515,7 +521,7 @@ const ConsumerGroupList = () => {
disabled={!proxyEnabled}
allowClear
/>
- <label style={{ marginRight: '8px', whiteSpace:
'nowrap' }}>{t.ENABLE_PROXY}:</label>
+ <label style={{marginRight: '8px', whiteSpace:
'nowrap'}}>{t.ENABLE_PROXY}:</label>
<Switch
checked={proxyEnabled}
onChange={(checked) => {
diff --git a/frontend-new/src/pages/Message/message.jsx
b/frontend-new/src/pages/Message/message.jsx
index 323a271..912d3f6 100644
--- a/frontend-new/src/pages/Message/message.jsx
+++ b/frontend-new/src/pages/Message/message.jsx
@@ -33,10 +33,9 @@ const MessageQueryPage = () => {
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 [timepickerBegin, setTimepickerBegin] =
useState(moment().subtract(1, 'hour'));
const [timepickerEnd, setTimepickerEnd] = useState(moment());
const [messageShowList, setMessageShowList] = useState([]);
const [paginationConf, setPaginationConf] = useState({
diff --git
a/src/main/java/org/apache/rocketmq/dashboard/controller/AclController.java
b/src/main/java/org/apache/rocketmq/dashboard/controller/AclController.java
index 787667a..3d7b72a 100644
--- a/src/main/java/org/apache/rocketmq/dashboard/controller/AclController.java
+++ b/src/main/java/org/apache/rocketmq/dashboard/controller/AclController.java
@@ -18,10 +18,10 @@
package org.apache.rocketmq.dashboard.controller;
import org.apache.rocketmq.dashboard.model.PolicyRequest;
+import org.apache.rocketmq.dashboard.model.UserInfoDto;
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.stereotype.Controller;
import org.springframework.web.bind.annotation.DeleteMapping;
@@ -44,16 +44,18 @@ public class AclController {
@GetMapping("/users.query")
@ResponseBody
- public List<UserInfo> listUsers(@RequestParam(required = false) String
brokerAddress) {
- return aclService.listUsers(brokerAddress);
+ public List<UserInfoDto> listUsers(@RequestParam(required = false) String
brokerName,
+ @RequestParam(required = false) String
clusterName) {
+ return aclService.listUsers(clusterName, brokerName);
}
@GetMapping("/acls.query")
@ResponseBody
public Object listAcls(
- @RequestParam(required = false) String brokerAddress,
- @RequestParam(required = false) String searchParam) {
- return aclService.listAcls(brokerAddress, searchParam);
+ @RequestParam(required = false) String brokerName,
+ @RequestParam(required = false) String searchParam,
+ @RequestParam(required = false) String clusterName) {
+ return aclService.listAcls(clusterName, brokerName, searchParam);
}
@PostMapping("/createAcl.do")
@@ -65,30 +67,35 @@ public class AclController {
@DeleteMapping("/deleteUser.do")
@ResponseBody
- public Object deleteUser(@RequestParam(required = false) String
brokerAddress, @RequestParam String username) {
- aclService.deleteUser(brokerAddress, username);
+ public Object deleteUser(@RequestParam(required = false) String brokerName,
+ @RequestParam String username,
+ @RequestParam(required = false) String
clusterName) {
+ aclService.deleteUser(clusterName, brokerName, username);
return true;
}
@RequestMapping(value = "/updateUser.do", method = RequestMethod.POST,
produces = "application/json;charset=UTF-8")
@ResponseBody
public Object updateUser(@RequestBody UserUpdateRequest request) {
- aclService.updateUser(request.getBrokerAddress(),
request.getUserInfo());
+ aclService.updateUser(request.getClusterName(),
request.getBrokerName(), request.getUserInfo());
return true;
}
@PostMapping("/createUser.do")
+ @ResponseBody
public Object createUser(@RequestBody UserCreateRequest request) {
- aclService.createUser(request.getBrokerAddress(),
request.getUserInfo());
+ aclService.createUser(request.getClusterName(),
request.getBrokerName(), request.getUserInfo());
return true;
}
@DeleteMapping("/deleteAcl.do")
+ @ResponseBody
public Object deleteAcl(
- @RequestParam(required = false) String brokerAddress,
+ @RequestParam(required = false) String brokerName,
+ @RequestParam(required = false) String clusterName,
@RequestParam String subject,
@RequestParam(required = false) String resource) {
- aclService.deleteAcl(brokerAddress, subject, resource);
+ aclService.deleteAcl(clusterName, brokerName, subject, resource);
return true;
}
diff --git a/src/main/java/org/apache/rocketmq/dashboard/model/AclInfo.java
b/src/main/java/org/apache/rocketmq/dashboard/model/AclInfo.java
new file mode 100644
index 0000000..87df246
--- /dev/null
+++ b/src/main/java/org/apache/rocketmq/dashboard/model/AclInfo.java
@@ -0,0 +1,209 @@
+/*
+ * 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 java.util.ArrayList;
+import java.util.Collections;
+import java.util.List;
+import java.util.Objects;
+import java.util.stream.Collectors;
+
+public class AclInfo {
+
+ private String subject;
+ private List<PolicyInfo> policies;
+
+ public static AclInfo of(String subject, List<String> resources,
List<String> actions,
+ List<String> sourceIps,
+ String decision) {
+ AclInfo aclInfo = new AclInfo();
+ aclInfo.setSubject(subject);
+ PolicyInfo policyInfo = PolicyInfo.of(resources, actions, sourceIps,
decision);
+ aclInfo.setPolicies(Collections.singletonList(policyInfo));
+ return aclInfo;
+ }
+
+ public static class PolicyInfo {
+
+ private String policyType;
+ private List<PolicyEntryInfo> entries;
+
+ public static PolicyInfo of(List<String> resources, List<String>
actions,
+ List<String> sourceIps, String decision) {
+ PolicyInfo policyInfo = new PolicyInfo();
+ List<PolicyEntryInfo> entries = resources.stream()
+ .map(resource -> PolicyEntryInfo.of(resource, actions,
sourceIps, decision))
+ .collect(Collectors.toList());
+ policyInfo.setEntries(entries);
+ return policyInfo;
+ }
+
+ public String getPolicyType() {
+ return policyType;
+ }
+
+ public void setPolicyType(String policyType) {
+ this.policyType = policyType;
+ }
+
+ public List<PolicyEntryInfo> getEntries() {
+ return entries;
+ }
+
+ public void setEntries(List<PolicyEntryInfo> entries) {
+ this.entries = entries;
+ }
+
+ @Override
+ public boolean equals(Object o) {
+ if (this == o) return true;
+ if (o == null || getClass() != o.getClass()) return false;
+ PolicyInfo that = (PolicyInfo) o;
+ return Objects.equals(policyType, that.policyType) &&
+ Objects.equals(entries, that.entries);
+ }
+
+ @Override
+ public int hashCode() {
+ return Objects.hash(policyType, entries);
+ }
+ }
+
+ public static class PolicyEntryInfo {
+ private String resource;
+ private List<String> actions;
+ private List<String> sourceIps;
+ private String decision;
+
+ public static PolicyEntryInfo of(String resource, List<String>
actions, List<String> sourceIps,
+ String decision) {
+ PolicyEntryInfo policyEntryInfo = new PolicyEntryInfo();
+ policyEntryInfo.setResource(resource);
+ policyEntryInfo.setActions(actions);
+ policyEntryInfo.setSourceIps(sourceIps);
+ policyEntryInfo.setDecision(decision);
+ return policyEntryInfo;
+ }
+
+ public String getResource() {
+ return resource;
+ }
+
+ public void setResource(String resource) {
+ this.resource = resource;
+ }
+
+ public List<String> getActions() {
+ return actions;
+ }
+
+ public void setActions(List<String> actions) {
+ this.actions = actions;
+ }
+
+ public List<String> getSourceIps() {
+ return sourceIps;
+ }
+
+ public void setSourceIps(List<String> sourceIps) {
+ this.sourceIps = sourceIps;
+ }
+
+ public String getDecision() {
+ return decision;
+ }
+
+ public void setDecision(String decision) {
+ this.decision = decision;
+ }
+
+ @Override
+ public boolean equals(Object o) {
+ if (this == o) return true;
+ if (o == null || getClass() != o.getClass()) return false;
+ PolicyEntryInfo that = (PolicyEntryInfo) o;
+ return Objects.equals(resource, that.resource) &&
+ Objects.equals(actions, that.actions) &&
+ Objects.equals(sourceIps, that.sourceIps) &&
+ Objects.equals(decision, that.decision);
+ }
+
+ @Override
+ public int hashCode() {
+ return Objects.hash(resource, actions, sourceIps, decision);
+ }
+ }
+
+ public String getSubject() {
+ return subject;
+ }
+
+ public void setSubject(String subject) {
+ this.subject = subject;
+ }
+
+ public List<PolicyInfo> getPolicies() {
+ return policies;
+ }
+
+ public void setPolicies(List<PolicyInfo> policies) {
+ this.policies = policies;
+ }
+
+ @Override
+ public boolean equals(Object o) {
+ if (this == o) return true;
+ if (o == null || getClass() != o.getClass()) return false;
+ AclInfo aclInfo = (AclInfo) o;
+ return Objects.equals(subject, aclInfo.subject) &&
+ Objects.equals(policies, aclInfo.policies);
+ }
+
+ @Override
+ public int hashCode() {
+ return Objects.hash(subject, policies);
+ }
+
+ public void copyFrom(org.apache.rocketmq.remoting.protocol.body.AclInfo
source) {
+ this.subject = source.getSubject();
+ if (source.getPolicies() != null) {
+ List<PolicyInfo> copiedPolicies = new ArrayList<>();
+ for (org.apache.rocketmq.remoting.protocol.body.AclInfo.PolicyInfo
policy : source.getPolicies()) {
+ PolicyInfo copiedPolicy = new PolicyInfo();
+ copiedPolicy.setPolicyType(policy.getPolicyType());
+ if (policy.getEntries() != null) {
+ List<PolicyEntryInfo> copiedEntries = new ArrayList<>();
+ for
(org.apache.rocketmq.remoting.protocol.body.AclInfo.PolicyEntryInfo entry :
policy.getEntries()) {
+ PolicyEntryInfo copiedEntry = new PolicyEntryInfo();
+ copiedEntry.setResource(entry.getResource());
+ copiedEntry.setActions(new
ArrayList<>(entry.getActions()));
+ copiedEntry.setSourceIps(new
ArrayList<>(entry.getSourceIps()));
+ copiedEntry.setDecision(entry.getDecision());
+ copiedEntries.add(copiedEntry);
+ }
+ copiedPolicy.setEntries(copiedEntries);
+ }
+ copiedPolicies.add(copiedPolicy);
+ }
+ this.setPolicies(copiedPolicies);
+ } else {
+ this.setPolicies(null);
+ }
+ }
+
+}
diff --git
a/src/main/java/org/apache/rocketmq/dashboard/model/PolicyRequest.java
b/src/main/java/org/apache/rocketmq/dashboard/model/PolicyRequest.java
index 43dcd0d..8eb6346 100644
--- a/src/main/java/org/apache/rocketmq/dashboard/model/PolicyRequest.java
+++ b/src/main/java/org/apache/rocketmq/dashboard/model/PolicyRequest.java
@@ -25,7 +25,8 @@ import java.util.List;
@Getter
@Setter
public class PolicyRequest {
- private String brokerAddress;
+ private String clusterName;
+ private String brokerName;
private String subject;
private List<Policy> policies;
}
diff --git
a/src/main/java/org/apache/rocketmq/dashboard/model/PolicyRequest.java
b/src/main/java/org/apache/rocketmq/dashboard/model/UserInfoDto.java
similarity index 57%
copy from src/main/java/org/apache/rocketmq/dashboard/model/PolicyRequest.java
copy to src/main/java/org/apache/rocketmq/dashboard/model/UserInfoDto.java
index 43dcd0d..c8e4074 100644
--- a/src/main/java/org/apache/rocketmq/dashboard/model/PolicyRequest.java
+++ b/src/main/java/org/apache/rocketmq/dashboard/model/UserInfoDto.java
@@ -17,15 +17,29 @@
package org.apache.rocketmq.dashboard.model;
-import lombok.Getter;
-import lombok.Setter;
+import lombok.AllArgsConstructor;
+import lombok.Data;
+import lombok.NoArgsConstructor;
+import org.apache.rocketmq.remoting.protocol.body.UserInfo;
-import java.util.List;
+@Data
+@NoArgsConstructor
+@AllArgsConstructor
+public class UserInfoDto {
-@Getter
-@Setter
-public class PolicyRequest {
- private String brokerAddress;
- private String subject;
- private List<Policy> policies;
+ private String username;
+
+ private String password;
+
+ private String userType;
+
+ private String userStatus;
+
+ public UserInfoDto setUserInfo(UserInfo userInfo) {
+ this.username = userInfo.getUsername();
+ this.password = userInfo.getPassword();
+ this.userType = userInfo.getUserType();
+ this.userStatus = userInfo.getUserStatus();
+ return this;
+ }
}
diff --git
a/src/main/java/org/apache/rocketmq/dashboard/model/request/UserCreateRequest.java
b/src/main/java/org/apache/rocketmq/dashboard/model/request/UserCreateRequest.java
index b0888d3..89ed851 100644
---
a/src/main/java/org/apache/rocketmq/dashboard/model/request/UserCreateRequest.java
+++
b/src/main/java/org/apache/rocketmq/dashboard/model/request/UserCreateRequest.java
@@ -23,6 +23,7 @@ import lombok.Setter;
@Setter
@Getter
public class UserCreateRequest {
- private String brokerAddress;
+ private String clusterName;
+ private String brokerName;
private UserInfoParam userInfo;
}
diff --git
a/src/main/java/org/apache/rocketmq/dashboard/model/request/UserUpdateRequest.java
b/src/main/java/org/apache/rocketmq/dashboard/model/request/UserUpdateRequest.java
index 9ef63f5..6c5caf7 100644
---
a/src/main/java/org/apache/rocketmq/dashboard/model/request/UserUpdateRequest.java
+++
b/src/main/java/org/apache/rocketmq/dashboard/model/request/UserUpdateRequest.java
@@ -23,6 +23,7 @@ import lombok.Setter;
@Getter
@Setter
public class UserUpdateRequest {
- private String brokerAddress;
+ private String clusterName;
+ private String brokerName;
private UserInfoParam userInfo;
}
diff --git
a/src/main/java/org/apache/rocketmq/dashboard/service/AclService.java
b/src/main/java/org/apache/rocketmq/dashboard/service/AclService.java
index fcc340c..c9089ae 100644
--- a/src/main/java/org/apache/rocketmq/dashboard/service/AclService.java
+++ b/src/main/java/org/apache/rocketmq/dashboard/service/AclService.java
@@ -20,24 +20,21 @@ package org.apache.rocketmq.dashboard.service;
import org.apache.rocketmq.dashboard.model.PolicyRequest;
import org.apache.rocketmq.dashboard.model.request.UserInfoParam;
-import org.apache.rocketmq.remoting.protocol.body.UserInfo;
-
-import java.util.List;
public interface AclService {
- List<UserInfo> listUsers(String brokerAddress);
+ Object listUsers(String clusterName, String brokerAddress);
- Object listAcls(String brokerAddress, String searchParam);
+ Object listAcls(String clusterName,String brokerAddress, String
searchParam);
- List<String> createAcl(PolicyRequest policyRequest);
+ Object createAcl(PolicyRequest policyRequest);
- void deleteUser(String brokerAddress, String username);
+ void deleteUser(String clusterName,String brokerAddress, String username);
- void updateUser(String brokerAddress, UserInfoParam userParam);
+ void updateUser(String clusterName,String brokerAddress, UserInfoParam
userParam);
- void createUser(String brokerAddress, UserInfoParam userParam);
+ void createUser(String clusterName,String brokerAddress, UserInfoParam
userParam);
- void deleteAcl(String brokerAddress, String subject, String resource);
+ void deleteAcl(String clusterName,String brokerAddress, String subject,
String resource);
void updateAcl(PolicyRequest policyRequest);
}
diff --git
a/src/main/java/org/apache/rocketmq/dashboard/service/impl/AclServiceImpl.java
b/src/main/java/org/apache/rocketmq/dashboard/service/impl/AclServiceImpl.java
index 52b9c94..cefcc44 100644
---
a/src/main/java/org/apache/rocketmq/dashboard/service/impl/AclServiceImpl.java
+++
b/src/main/java/org/apache/rocketmq/dashboard/service/impl/AclServiceImpl.java
@@ -21,9 +21,13 @@ import com.fasterxml.jackson.databind.ObjectMapper;
import org.apache.rocketmq.dashboard.model.Entry;
import org.apache.rocketmq.dashboard.model.Policy;
import org.apache.rocketmq.dashboard.model.PolicyRequest;
+import org.apache.rocketmq.dashboard.model.UserInfoDto;
import org.apache.rocketmq.dashboard.model.request.UserInfoParam;
+import org.apache.rocketmq.dashboard.service.AbstractCommonService;
import org.apache.rocketmq.dashboard.service.AclService;
+import org.apache.rocketmq.dashboard.service.ClusterInfoService;
import org.apache.rocketmq.remoting.protocol.body.AclInfo;
+import org.apache.rocketmq.remoting.protocol.body.ClusterInfo;
import org.apache.rocketmq.remoting.protocol.body.UserInfo;
import org.apache.rocketmq.tools.admin.MQAdminExt;
import org.slf4j.Logger;
@@ -37,7 +41,7 @@ import java.util.List;
import java.util.Set;
@Service
-public class AclServiceImpl implements AclService {
+public class AclServiceImpl extends AbstractCommonService implements
AclService {
private Logger logger = LoggerFactory.getLogger(AclServiceImpl.class);
@@ -45,60 +49,117 @@ public class AclServiceImpl implements AclService {
@Autowired
private MQAdminExt mqAdminExt;
+ @Autowired
+ private ClusterInfoService clusterInfoService;
- @Override
- public List<UserInfo> listUsers(String brokerAddress) {
- List<UserInfo> userList;
- try {
- userList = mqAdminExt.listUser(brokerAddress, "");
- } catch (Exception ex) {
- logger.error("Failed to list users from broker: {}",
brokerAddress, ex);
- throw new RuntimeException("Failed to list users", ex);
- }
- if (userList == null || userList.isEmpty()) {
- logger.warn("No users found for broker: {}", brokerAddress);
- return new ArrayList<>();
- }
- return userList;
- }
@Override
- public Object listAcls(String brokerAddress, String searchParam) {
- List<AclInfo> aclList;
- try {
- String user = searchParam != null ? searchParam : "";
- String res = searchParam != null ? searchParam : "";
- aclList = mqAdminExt.listAcl(brokerAddress, user, "");
- if (aclList == null) {
- aclList = new ArrayList<>();
+ public List<UserInfoDto> listUsers(String clusterName, String brokerName) {
+
+ List<String> brokerAddrList = getBrokerAddressList(clusterName,
brokerName);
+ Set<UserInfoDto> commonUsers = new HashSet<>();
+ final boolean[] firstIteration = {true};
+ brokerAddrList.forEach(address -> {
+ List<UserInfo> userList;
+ try {
+ userList = mqAdminExt.listUser(address, "");
+ } catch (Exception ex) {
+ logger.error("Failed to list users from broker: {}", address,
ex);
+ throw new RuntimeException("Failed to list users", ex);
}
- List<AclInfo> resAclList = mqAdminExt.listAcl(brokerAddress, "",
res);
- if (resAclList != null) {
- aclList.addAll(resAclList);
+
+ List<UserInfoDto> userListDtos = new ArrayList<>();
+ userList.forEach(user -> {
+ UserInfoDto userInfoDto = new UserInfoDto();
+ userListDtos.add(userInfoDto.setUserInfo(user));
+ });
+ if (!userList.isEmpty()) {
+ Set<UserInfoDto> currentUsers = new HashSet<>(userListDtos);
+ if (firstIteration[0]) {
+ commonUsers.addAll(userListDtos);
+ firstIteration[0] = false;
+ } else {
+ commonUsers.retainAll(currentUsers);
+ }
+ } else {
+ logger.warn("No users found for broker: {}", address);
}
- } catch (Exception ex) {
- logger.error("Failed to list ACLs from broker: {}", brokerAddress,
ex);
- throw new RuntimeException("Failed to list ACLs", ex);
- }
- ObjectMapper mapper = new ObjectMapper();
- Set<String> uniqueAclStrings = new HashSet<>();
- List<AclInfo> resultAclList = new ArrayList<>();
+ });
+
+ return new ArrayList<>(commonUsers);
+ }
- for (AclInfo acl : aclList) {
+ @Override
+ public Object listAcls(String clusterName, String brokerName, String
searchParam) {
+ List<String> brokerAddrList = getBrokerAddressList(clusterName,
brokerName);
+ Set<org.apache.rocketmq.dashboard.model.AclInfo> commonAcls = new
HashSet<>();
+ final boolean[] firstIteration = {true};
+ ObjectMapper mapper = new ObjectMapper(); // Initialize ObjectMapper
once
+
+ brokerAddrList.forEach(address -> {
+ List<AclInfo> aclListForBroker;
try {
- String aclString = mapper.writeValueAsString(acl);
- if (uniqueAclStrings.add(aclString)) {
- resultAclList.add(acl);
+ String user = searchParam != null ? searchParam : "";
+ String res = searchParam != null ? searchParam : "";
+ // Combine results from both listAcl calls for a single broker
+ List<AclInfo> byUser = mqAdminExt.listAcl(address, user, "");
+ List<AclInfo> byRes = mqAdminExt.listAcl(address, "", res);
+
+ aclListForBroker = new ArrayList<>();
+ if (byUser != null) {
+ aclListForBroker.addAll(byUser);
+ }
+ if (byRes != null) {
+ aclListForBroker.addAll(byRes);
+ }
+
+ // Deduplicate ACLs for the current broker to ensure accurate
intersection
+ Set<AclInfo> uniqueAclsForBroker = new HashSet<>();
+ Set<String> uniqueAclStringsForBroker = new HashSet<>();
+ for (AclInfo acl : aclListForBroker) {
+ try {
+ String aclString = mapper.writeValueAsString(acl);
+ if (uniqueAclStringsForBroker.add(aclString)) {
+ uniqueAclsForBroker.add(acl);
+ }
+ } catch (Exception e) {
+ logger.error("Error serializing AclInfo for broker {}:
{}", address, e.getMessage());
+ }
}
- } catch (Exception e) {
- logger.error("Error serializing AclInfo", e);
+ aclListForBroker = new ArrayList<>(uniqueAclsForBroker);
+
+ } catch (Exception ex) {
+ logger.error("Failed to list ACLs from broker: {}", address,
ex);
+ throw new RuntimeException("Failed to list ACLs", ex);
}
- }
- return resultAclList;
+ List<org.apache.rocketmq.dashboard.model.AclInfo> aclInfoList =
new ArrayList<>();
+ aclListForBroker.forEach(acl -> {
+ org.apache.rocketmq.dashboard.model.AclInfo aclInfo = new
org.apache.rocketmq.dashboard.model.AclInfo();
+ aclInfo.copyFrom(acl);
+ aclInfoList.add(aclInfo);
+ });
+ if (!aclListForBroker.isEmpty()) {
+ Set<org.apache.rocketmq.dashboard.model.AclInfo> currentAcls =
new HashSet<>(aclInfoList);
+ if (firstIteration[0]) {
+ commonAcls.addAll(currentAcls);
+ firstIteration[0] = false;
+ } else {
+ commonAcls.retainAll(currentAcls);
+ }
+ } else {
+ logger.warn("No ACLs found for broker: {}", address);
+ if (firstIteration[0]) {
+ firstIteration[0] = false;
+ } else {
+ commonAcls.clear(); // If any broker has no ACLs, the
common set will be empty
+ }
+ }
+ });
+ return new ArrayList<>(commonAcls);
}
@Override
- public List<String> createAcl(PolicyRequest policyRequest) {
+ public Object createAcl(PolicyRequest policyRequest) {
List<String> successfulResources = new ArrayList<>();
if (policyRequest == null || policyRequest.getPolicies() == null ||
policyRequest.getPolicies().isEmpty()) {
@@ -107,11 +168,13 @@ public class AclServiceImpl implements AclService {
}
String subject = policyRequest.getSubject();
-
if (subject == null || subject.isEmpty()) {
throw new IllegalArgumentException("Subject cannot be null or
empty.");
}
+ // Get the broker address list for creating ACLs on all relevant
brokers
+ List<String> brokerAddrList =
getBrokerAddressList(policyRequest.getClusterName(),
policyRequest.getBrokerName());
+
for (Policy policy : policyRequest.getPolicies()) {
if (policy.getEntries() != null && !policy.getEntries().isEmpty())
{
for (Entry entry : policy.getEntries()) {
@@ -136,90 +199,110 @@ public class AclServiceImpl implements AclService {
aclInfo.setPolicies(aclPolicies);
aclInfo.setSubject(subject);
- try {
- logger.info("Attempting to create ACL for
subject: {}, resource: {} on broker: {}", subject, resource,
policyRequest.getBrokerAddress());
-
mqAdminExt.createAcl(policyRequest.getBrokerAddress(), aclInfo);
- successfulResources.add(resource);
- logger.info("Successfully created ACL for
subject: {}, resource: {}", subject, resource);
- } catch (Exception ex) {
- logger.error("Failed to create ACL for
subject: {}, resource: {} on broker: {}", subject, resource,
policyRequest.getBrokerAddress(), ex);
- throw new RuntimeException("Failed to create
ACL", ex);
+ for (String brokerAddress : brokerAddrList) {
+ try {
+ logger.info("Attempting to create ACL for
subject: {}, resource: {} on broker: {}", subject, resource, brokerAddress);
+ mqAdminExt.createAcl(brokerAddress,
aclInfo);
+ logger.info("Successfully created ACL for
subject: {}, resource: {} on broker: {}", subject, resource, brokerAddress);
+ } catch (Exception ex) {
+ throw new RuntimeException("Failed to
create ACL on broker " + brokerAddress + ex.getMessage());
+ }
}
}
}
}
}
}
- return successfulResources;
+ return true;
}
@Override
- public void deleteUser(String brokerAddress, String username) {
- try {
- mqAdminExt.deleteUser(brokerAddress, username);
- } catch (Exception ex) {
- logger.error("Failed to delete user: {} from broker: {}",
username, brokerAddress, ex);
- throw new RuntimeException("Failed to delete user", ex);
+ public void deleteUser(String clusterName, String brokerName, String
username) {
+ List<String> brokerAddrList = getBrokerAddressList(clusterName,
brokerName);
+
+ for (String address : brokerAddrList) {
+ try {
+ mqAdminExt.deleteUser(address, username);
+ logger.info("Successfully deleted user: {} from broker: {}",
username, address);
+ } catch (Exception ex) {
+ logger.error("Failed to delete user: {} from broker: {}",
username, address, ex);
+ throw new RuntimeException("Failed to delete user on broker "
+ address + ex.getMessage());
+ }
}
}
@Override
- public void updateUser(String brokerAddress, UserInfoParam userParam) {
+ public void updateUser(String clusterName, String brokerName,
UserInfoParam userParam) {
UserInfo user = new UserInfo();
user.setUsername(userParam.getUsername());
user.setPassword(userParam.getPassword());
user.setUserStatus(userParam.getUserStatus());
user.setUserType(userParam.getUserType());
- try {
- mqAdminExt.updateUser(brokerAddress, user);
- } catch (Exception ex) {
- logger.error("Failed to update user: {} on broker: {}",
userParam.getUsername(), brokerAddress, ex);
- throw new RuntimeException("Failed to update user", ex);
+ List<String> brokerAddrList = getBrokerAddressList(clusterName,
brokerName);
+
+ for (String address : brokerAddrList) {
+ try {
+ mqAdminExt.updateUser(address, user);
+ logger.info("Successfully updated user: {} on broker: {}",
userParam.getUsername(), address);
+ } catch (Exception ex) {
+ logger.error("Failed to update user: {} on broker: {}",
userParam.getUsername(), address, ex);
+ throw new RuntimeException("Failed to update user on broker "
+ address + ex.getMessage());
+ }
}
}
@Override
- public void createUser(String brokerAddress, UserInfoParam userParam) {
+ public void createUser(String clusterName, String brokerName,
UserInfoParam userParam) {
UserInfo user = new UserInfo();
user.setUsername(userParam.getUsername());
user.setPassword(userParam.getPassword());
user.setUserStatus(userParam.getUserStatus());
user.setUserType(userParam.getUserType());
- try {
- mqAdminExt.createUser(brokerAddress, user);
- } catch (Exception ex) {
- logger.error("Failed to create user: {} on broker: {}",
userParam.getUsername(), brokerAddress, ex);
- throw new RuntimeException("Failed to create user", ex);
+
+ List<String> brokerAddrList = getBrokerAddressList(clusterName,
brokerName);
+
+ for (String address : brokerAddrList) {
+ try {
+ mqAdminExt.createUser(address, user);
+ logger.info("Successfully created user: {} on broker: {}",
userParam.getUsername(), address);
+ } catch (Exception ex) {
+ logger.error("Failed to create user: {} on broker: {}",
userParam.getUsername(), address, ex);
+ throw new RuntimeException("Failed to create user on broker "
+ address + ex.getMessage());
+ }
}
}
@Override
- public void deleteAcl(String brokerAddress, String subject, String
resource) {
- try {
- String res = resource != null ? resource : "";
- mqAdminExt.deleteAcl(brokerAddress, subject, res);
- } catch (Exception ex) {
- logger.error("Failed to delete ACL for subject: {} and resource:
{} on broker: {}", subject, resource, brokerAddress, ex);
- throw new RuntimeException("Failed to delete ACL", ex);
+ public void deleteAcl(String clusterName, String brokerName, String
subject, String resource) {
+ List<String> brokerAddrList = getBrokerAddressList(clusterName,
brokerName);
+ String res = resource != null ? resource : "";
+
+ for (String address : brokerAddrList) {
+ try {
+ mqAdminExt.deleteAcl(address, subject, res);
+ logger.info("Successfully deleted ACL for subject: {} and
resource: {} on broker: {}", subject, resource, address);
+ } catch (Exception ex) {
+ logger.error("Failed to delete ACL for subject: {} and
resource: {} on broker: {}", subject, resource, address, ex);
+ throw new RuntimeException("Failed to delete ACL on broker " +
address + ex.getMessage());
+ }
}
}
@Override
public void updateAcl(PolicyRequest policyRequest) {
-
if (policyRequest == null || policyRequest.getPolicies() == null ||
policyRequest.getPolicies().isEmpty()) {
logger.warn("Policy request is null or policies list is empty. No
ACLs to update.");
+ return;
}
- assert policyRequest != null;
- String brokerAddress = policyRequest.getBrokerAddress();
String subject = policyRequest.getSubject();
-
if (subject == null || subject.isEmpty()) {
throw new IllegalArgumentException("Subject cannot be null or
empty.");
}
+ List<String> brokerAddrList =
getBrokerAddressList(policyRequest.getClusterName(),
policyRequest.getBrokerName());
+
for (Policy policy : policyRequest.getPolicies()) {
if (policy.getEntries() != null && !policy.getEntries().isEmpty())
{
for (Entry entry : policy.getEntries()) {
@@ -244,18 +327,52 @@ public class AclServiceImpl implements AclService {
aclInfo.setPolicies(aclPolicies);
aclInfo.setSubject(subject);
- try {
- mqAdminExt.updateAcl(brokerAddress, aclInfo);
- } catch (Exception ex) {
- logger.error("Failed to update ACL for
subject: {} on broker: {}", subject, brokerAddress, ex);
- throw new RuntimeException("Failed to update
ACL", ex);
+ for (String brokerAddress : brokerAddrList) {
+ try {
+ mqAdminExt.updateAcl(brokerAddress,
aclInfo);
+ logger.info("Successfully updated ACL for
subject: {}, resource: {} on broker: {}", subject, resource, brokerAddress);
+ } catch (Exception ex) {
+ logger.error("Failed to update ACL for
subject: {}, resource: {} on broker: {}", subject, resource, brokerAddress, ex);
+ throw new RuntimeException("Failed to
update ACL on broker " + brokerAddress + ex.getMessage());
+ }
}
}
}
}
}
}
+ }
+
+ public List<String> getBrokerAddressList(String clusterName, String
brokerName) {
+ ClusterInfo clusterInfo = clusterInfoService.get();
+ List<String> brokerAddressList = new ArrayList<>();
+ if (brokerName != null) {
+ for (String brokerNameKey :
changeToBrokerNameSet(clusterInfo.getClusterAddrTable(),
+ new ArrayList<>(), List.of(brokerName))) {
+ clusterInfo.getBrokerAddrTable()
+ .get(brokerNameKey)
+ .getBrokerAddrs()
+ .forEach((Long key, String value) ->
brokerAddressList.add(value));
+ }
+ } else {
+ if (clusterName == null || clusterName.isEmpty()) {
+ logger.warn("Cluster name is null or empty. Cannot retrieve
broker addresses.");
+ throw new IllegalArgumentException("Cluster name cannot be
null or empty.");
+ }
+ if (clusterInfo == null || clusterInfo.getBrokerAddrTable() ==
null || clusterInfo.getBrokerAddrTable().isEmpty()) {
+ logger.warn("Cluster information is not available or has no
broker addresses.");
+ throw new RuntimeException("Cluster information is not
available or has no broker addresses.");
+ }
+ for (String brokerNameKey :
changeToBrokerNameSet(clusterInfo.getClusterAddrTable(),
+ List.of(clusterName), new ArrayList<>())) {
+ clusterInfo.getBrokerAddrTable()
+ .get(brokerNameKey)
+ .getBrokerAddrs()
+ .forEach((Long key, String value) ->
brokerAddressList.add(value));
+ }
+ }
+ return brokerAddressList;
}
}
diff --git
a/src/main/java/org/apache/rocketmq/dashboard/service/impl/ConsumerServiceImpl.java
b/src/main/java/org/apache/rocketmq/dashboard/service/impl/ConsumerServiceImpl.java
index 93b39e7..e307966 100644
---
a/src/main/java/org/apache/rocketmq/dashboard/service/impl/ConsumerServiceImpl.java
+++
b/src/main/java/org/apache/rocketmq/dashboard/service/impl/ConsumerServiceImpl.java
@@ -210,7 +210,15 @@ public class ConsumerServiceImpl extends
AbstractCommonService implements Consum
if (SYSTEM_GROUP_SET.contains(consumerGroup)) {
consumeInfo.setSubGroupType("SYSTEM");
} else {
-
consumeInfo.setSubGroupType(subscriptionGroupTable.get(consumerGroup).isConsumeMessageOrderly()
? "FIFO" : "NORMAL");
+ try {
+
consumeInfo.setSubGroupType(subscriptionGroupTable.get(consumerGroup).isConsumeMessageOrderly()
? "FIFO" : "NORMAL");
+ } catch (NullPointerException e) {
+ logger.warn("SubscriptionGroupConfig not found for
consumer group: {}", consumerGroup);
+ boolean isFifoType =
examineSubscriptionGroupConfig(consumerGroup)
+
.stream().map(ConsumerConfigInfo::getSubscriptionGroupConfig)
+
.allMatch(SubscriptionGroupConfig::isConsumeMessageOrderly);
+ consumeInfo.setSubGroupType(isFifoType ? "FIFO" :
"NORMAL");
+ }
}
consumeInfo.setUpdateTime(new Date());
groupConsumeInfoList.add(consumeInfo);
@@ -275,17 +283,24 @@ public class ConsumerServiceImpl extends
AbstractCommonService implements Consum
@Override
public List<TopicConsumerInfo> queryConsumeStatsListByGroupName(String
groupName, String address) {
- ConsumeStats consumeStats;
+ List<ConsumeStats> consumeStatses = new ArrayList<>();
String topic = null;
try {
String[] addresses = address.split(",");
- String addr = addresses[0];
- consumeStats = mqAdminExt.examineConsumeStats(addr, groupName,
null, 3000);
+ for (String addr : addresses) {
+ consumeStatses.add(mqAdminExt.examineConsumeStats(addr,
groupName, null, 3000));
+ }
} catch (Exception e) {
Throwables.throwIfUnchecked(e);
throw new RuntimeException(e);
}
- return toTopicConsumerInfoList(topic, consumeStats, groupName);
+ List<TopicConsumerInfo> res = new ArrayList<>();
+ consumeStatses.forEach(consumeStats -> {
+ if (consumeStats != null && consumeStats.getOffsetTable() != null
&& !consumeStats.getOffsetTable().isEmpty()) {
+ res.addAll(toTopicConsumerInfoList(topic, consumeStats,
groupName));
+ }
+ });
+ return res;
}
@Override
diff --git
a/src/main/java/org/apache/rocketmq/dashboard/service/provider/UserInfoProviderImpl.java
b/src/main/java/org/apache/rocketmq/dashboard/service/provider/UserInfoProviderImpl.java
index a5aaf6b..fd6a3f5 100644
---
a/src/main/java/org/apache/rocketmq/dashboard/service/provider/UserInfoProviderImpl.java
+++
b/src/main/java/org/apache/rocketmq/dashboard/service/provider/UserInfoProviderImpl.java
@@ -16,15 +16,16 @@
*/
package org.apache.rocketmq.dashboard.service.provider;
+
import org.apache.rocketmq.dashboard.service.ClusterInfoService;
import org.apache.rocketmq.remoting.protocol.body.ClusterInfo;
import org.apache.rocketmq.remoting.protocol.body.UserInfo;
import org.apache.rocketmq.remoting.protocol.route.BrokerData;
import org.apache.rocketmq.tools.admin.MQAdminExt;
-import org.springframework.beans.factory.annotation.Autowired;
-import org.springframework.stereotype.Service;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.stereotype.Service;
@Service
public class UserInfoProviderImpl implements UserInfoProvider {
diff --git
a/src/test/java/org/apache/rocketmq/dashboard/controller/AclControllerTest.java
b/src/test/java/org/apache/rocketmq/dashboard/controller/AclControllerTest.java
index b8afa10..cf368d7 100644
---
a/src/test/java/org/apache/rocketmq/dashboard/controller/AclControllerTest.java
+++
b/src/test/java/org/apache/rocketmq/dashboard/controller/AclControllerTest.java
@@ -17,32 +17,33 @@
package org.apache.rocketmq.dashboard.controller;
+import com.google.gson.Gson;
import org.apache.rocketmq.auth.authentication.enums.UserStatus;
import org.apache.rocketmq.auth.authentication.enums.UserType;
-import org.apache.rocketmq.auth.authorization.enums.Decision;
-import org.apache.rocketmq.dashboard.model.Policy;
-import org.apache.rocketmq.dashboard.model.PolicyRequest;
+import org.apache.rocketmq.dashboard.model.UserInfoDto;
import org.apache.rocketmq.dashboard.model.request.UserCreateRequest;
import org.apache.rocketmq.dashboard.model.request.UserInfoParam;
import org.apache.rocketmq.dashboard.model.request.UserUpdateRequest;
import org.apache.rocketmq.dashboard.service.impl.AclServiceImpl;
import org.apache.rocketmq.dashboard.support.GlobalExceptionHandler;
-import org.apache.rocketmq.remoting.protocol.body.AclInfo;
-import org.apache.rocketmq.remoting.protocol.body.UserInfo;
import org.junit.Before;
import org.junit.Test;
import org.mockito.InjectMocks;
import org.mockito.Mock;
import org.mockito.MockitoAnnotations;
+import org.springframework.http.MediaType;
+import org.springframework.test.web.servlet.request.MockMvcRequestBuilders;
import org.springframework.test.web.servlet.setup.MockMvcBuilders;
-
import java.util.Arrays;
import java.util.List;
import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.mockito.Mockito.doNothing;
import static org.mockito.Mockito.times;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;
+import static
org.springframework.test.web.servlet.request.MockMvcRequestBuilders.delete;
+import static
org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;
public class AclControllerTest extends BaseControllerTest {
@@ -52,189 +53,193 @@ public class AclControllerTest extends BaseControllerTest
{
@InjectMocks
private AclController aclController;
+ private final Gson gson = new Gson();
+
@Before
public void init() {
MockitoAnnotations.initMocks(this);
mockMvc =
MockMvcBuilders.standaloneSetup(aclController).setControllerAdvice(GlobalExceptionHandler.class).build();
}
-
@Test
- public void testListUsers() {
+ public void testListUsers() throws Exception {
// Prepare test data
- String brokerAddress = "localhost:10911";
- List<UserInfo> expectedUsers = Arrays.asList(
- UserInfo.of("user1", "password1", "super"),
- UserInfo.of("user2", "password2", "super")
+ String clusterName = "test-cluster";
+ String brokerName = "localhost:10911";
+ List<UserInfoDto> expectedUsers = Arrays.asList(
+ new UserInfoDto("user1", "password1", "super","enable"),
+ new UserInfoDto("user2", "password2", "super","enable")
);
// Mock service behavior
- when(aclService.listUsers(brokerAddress)).thenReturn(expectedUsers);
-
- // Call controller method
- List<UserInfo> result = aclController.listUsers(brokerAddress);
-
- // Verify
- assertEquals(expectedUsers, result);
- verify(aclService, times(1)).listUsers(brokerAddress);
- }
+ when(aclService.listUsers(clusterName,
brokerName)).thenReturn(expectedUsers);
+
+ // Call controller method via MockMVC
+ mockMvc.perform(MockMvcRequestBuilders.get("/acl/users.query")
+ .param("clusterName", clusterName)
+ .param("brokerName", brokerName))
+ .andExpect(status().isOk())
+ .andExpect(result -> {
+ List<UserInfoDto> actualUsers =
gson.fromJson(result.getResponse().getContentAsString(), List.class);
+ // Due to Gson's deserialization of List to LinkedTreeMap,
direct assertEquals on List<UserInfo> won't work easily.
+ // A more robust comparison would involve iterating or
using a custom matcher if UserInfoDto doesn't override equals/hashCode.
+ // For simplicity, let's assume UserInfoDto has proper
equals/hashCode for now or convert to JSON string for comparison.
+ assertEquals(gson.toJson(expectedUsers),
result.getResponse().getContentAsString());
+ });
- @Test
- public void testListUsersWithoutBrokerAddress() {
- // Prepare test data
- List<UserInfo> expectedUsers = Arrays.asList(
- UserInfo.of("user1", "password1", "super")
- );
-
- // Mock service behavior
- when(aclService.listUsers(null)).thenReturn(expectedUsers);
- // Call controller method
- List<UserInfo> result = aclController.listUsers(null);
// Verify
- assertEquals(expectedUsers, result);
- verify(aclService, times(1)).listUsers(null);
+ verify(aclService, times(1)).listUsers(clusterName, brokerName);
}
@Test
- public void testListAcls() {
+ public void testListUsersWithoutBrokerAddressAndClusterName() throws
Exception {
// Prepare test data
- String brokerAddress = "localhost:9092";
- String searchParam = "user1";
- Object expectedAcls = Arrays.asList(
- AclInfo.of("user1", List.of("READ", "test"),
List.of("TOPIC:test"), List.of("localhost:10911"), Decision.ALLOW.getName())
+ List<UserInfoDto> expectedUsers = Arrays.asList(
+ new UserInfoDto("user2", "password2", "super","enable")
);
// Mock service behavior
- when(aclService.listAcls(brokerAddress,
searchParam)).thenReturn(expectedAcls);
+ when(aclService.listUsers(null, null)).thenReturn(expectedUsers);
- // Call controller method
- Object result = aclController.listAcls(brokerAddress, searchParam);
+ // Call controller method via MockMVC
+ mockMvc.perform(MockMvcRequestBuilders.get("/acl/users.query"))
+ .andExpect(status().isOk())
+ .andExpect(result -> assertEquals(gson.toJson(expectedUsers),
result.getResponse().getContentAsString()));
// Verify
- assertEquals(expectedAcls, result);
- verify(aclService, times(1)).listAcls(brokerAddress, searchParam);
+ verify(aclService, times(1)).listUsers(null, null);
}
- @Test
- public void testCreateAcl() {
- // Prepare test data
- PolicyRequest request = new PolicyRequest();
- request.setBrokerAddress("localhost:9092");
- request.setSubject("user1");
- request.setPolicies(List.of(
- new Policy()
- ));
- // Call controller method
- Object result = aclController.createAcl(request);
- // Verify
- assertEquals(true, result);
- verify(aclService, times(1)).createAcl(request);
- }
@Test
- public void testDeleteUser() {
+ public void testDeleteUser() throws Exception {
// Prepare test data
- String brokerAddress = "localhost:9092";
+ String clusterName = "test-cluster";
+ String brokerName = "localhost:9092";
String username = "user1";
- // Call controller method
- Object result = aclController.deleteUser(brokerAddress, username);
+ // Mock service behavior (void method)
+ doNothing().when(aclService).deleteUser(clusterName, brokerName,
username);
+
+ // Call controller method via MockMVC
+ mockMvc.perform(delete("/acl/deleteUser.do")
+ .param("clusterName", clusterName)
+ .param("brokerName", brokerName)
+ .param("username", username))
+ .andExpect(status().isOk())
+ .andExpect(result -> assertEquals("true",
result.getResponse().getContentAsString()));
// Verify
- assertEquals(true, result);
- verify(aclService, times(1)).deleteUser(brokerAddress, username);
+ verify(aclService, times(1)).deleteUser(clusterName, brokerName,
username);
}
@Test
- public void testDeleteUserWithoutBrokerAddress() {
+ public void testDeleteUserWithoutBrokerAddressAndClusterName() throws
Exception {
// Prepare test data
String username = "user1";
- // Call controller method
- Object result = aclController.deleteUser(null, username);
+ // Mock service behavior (void method)
+ doNothing().when(aclService).deleteUser(null, null, username);
+
+ // Call controller method via MockMVC
+ mockMvc.perform(delete("/acl/deleteUser.do")
+ .param("username", username))
+ .andExpect(status().isOk())
+ .andExpect(result -> assertEquals("true",
result.getResponse().getContentAsString()));
// Verify
- assertEquals(true, result);
- verify(aclService, times(1)).deleteUser(null, username);
+ verify(aclService, times(1)).deleteUser(null, null, username);
}
@Test
- public void testUpdateUser() {
+ public void testUpdateUser() throws Exception {
// Prepare test data
UserUpdateRequest request = new UserUpdateRequest();
- request.setBrokerAddress("localhost:9092");
+ request.setClusterName("test-cluster");
+ request.setBrokerName("localhost:9092");
request.setUserInfo(new UserInfoParam("user1", "newPassword",
UserStatus.ENABLE.getName(), UserType.SUPER.getName()));
- // Call controller method
- Object result = aclController.updateUser(request);
+ // Mock service behavior (void method)
+ doNothing().when(aclService).updateUser(request.getClusterName(),
request.getBrokerName(), request.getUserInfo());
+
+ // Call controller method via MockMVC
+ mockMvc.perform(MockMvcRequestBuilders.post("/acl/updateUser.do")
+ .contentType(MediaType.APPLICATION_JSON)
+ .content(gson.toJson(request)))
+ .andExpect(status().isOk())
+ .andExpect(result -> assertEquals("true",
result.getResponse().getContentAsString()));
// Verify
- assertEquals(true, result);
- verify(aclService, times(1)).updateUser(request.getBrokerAddress(),
request.getUserInfo());
+ verify(aclService, times(1)).updateUser(request.getClusterName(),
request.getBrokerName(), request.getUserInfo());
}
@Test
- public void testCreateUser() {
+ public void testCreateUser() throws Exception {
// Prepare test data
UserCreateRequest request = new UserCreateRequest();
- request.setBrokerAddress("localhost:9092");
+ request.setClusterName("test-cluster");
+ request.setBrokerName("localhost:9092");
request.setUserInfo(new UserInfoParam("user1", "newPassword",
UserStatus.ENABLE.getName(), UserType.SUPER.getName()));
- // Call controller method
- Object result = aclController.createUser(request);
+ // Mock service behavior (void method)
+ doNothing().when(aclService).createUser(request.getClusterName(),
request.getBrokerName(), request.getUserInfo());
+
+ // Call controller method via MockMVC
+ mockMvc.perform(MockMvcRequestBuilders.post("/acl/createUser.do")
+ .contentType(MediaType.APPLICATION_JSON)
+ .content(gson.toJson(request)))
+ .andExpect(status().isOk())
+ .andExpect(result -> assertEquals("true",
result.getResponse().getContentAsString()));
// Verify
- assertEquals(true, result);
- verify(aclService, times(1)).createUser(request.getBrokerAddress(),
request.getUserInfo());
+ verify(aclService, times(1)).createUser(request.getClusterName(),
request.getBrokerName(), request.getUserInfo());
}
@Test
- public void testDeleteAcl() {
+ public void testDeleteAcl() throws Exception {
// Prepare test data
- String brokerAddress = "localhost:9092";
+ String clusterName = "test-cluster";
+ String brokerName = "localhost:9092";
String subject = "user1";
String resource = "TOPIC:test";
- // Call controller method
- Object result = aclController.deleteAcl(brokerAddress, subject,
resource);
+ // Mock service behavior (void method)
+ doNothing().when(aclService).deleteAcl(clusterName, brokerName,
subject, resource);
+
+ // Call controller method via MockMVC
+ mockMvc.perform(delete("/acl/deleteAcl.do")
+ .param("clusterName", clusterName)
+ .param("brokerName", brokerName)
+ .param("subject", subject)
+ .param("resource", resource))
+ .andExpect(status().isOk())
+ .andExpect(result -> assertEquals("true",
result.getResponse().getContentAsString()));
// Verify
- assertEquals(true, result);
- verify(aclService, times(1)).deleteAcl(brokerAddress, subject,
resource);
+ verify(aclService, times(1)).deleteAcl(clusterName, brokerName,
subject, resource);
}
@Test
- public void testDeleteAclWithoutBrokerAddressAndResource() {
+ public void testDeleteAclWithoutBrokerAddressAndResourceAndClusterName()
throws Exception {
// Prepare test data
String subject = "user1";
- // Call controller method
- Object result = aclController.deleteAcl(null, subject, null);
+ // Mock service behavior (void method)
+ doNothing().when(aclService).deleteAcl(null, null, subject, null);
+
+ // Call controller method via MockMVC
+ mockMvc.perform(delete("/acl/deleteAcl.do")
+ .param("subject", subject))
+ .andExpect(status().isOk())
+ .andExpect(result -> assertEquals("true",
result.getResponse().getContentAsString()));
// Verify
- assertEquals(true, result);
- verify(aclService, times(1)).deleteAcl(null, subject, null);
+ verify(aclService, times(1)).deleteAcl(null, null, subject, null);
}
- @Test
- public void testUpdateAcl() {
- // Prepare test data
- PolicyRequest request = new PolicyRequest();
- request.setBrokerAddress("localhost:9092");
- request.setSubject("user1");
- request.setPolicies(List.of(
- new Policy()
- ));
-
- // Call controller method
- Object result = aclController.updateAcl(request);
- // Verify
- assertEquals(true, result);
- verify(aclService, times(1)).updateAcl(request);
- }
@Override
protected Object getTestController() {