From a4e02f472fb666e8f149bd07ebbeff8c7e6629ba Mon Sep 17 00:00:00 2001 From: Crazylychee <110229037+Crazylychee@users.noreply.github.com> Date: Tue, 8 Jul 2025 10:46:25 +0800 Subject: [PATCH] [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 | 54 +- .../src/components/acl/SubjectInput.jsx | 18 +- .../components/consumer/ClientInfoModal.jsx | 117 +++- .../consumer/ConsumerDetailModal.jsx | 88 ++- frontend-new/src/i18n/index.js | 35 ++ frontend-new/src/pages/Acl/acl.jsx | 554 +++++++++--------- frontend-new/src/pages/Consumer/consumer.jsx | 36 +- frontend-new/src/pages/Message/message.jsx | 3 +- .../dashboard/controller/AclController.java | 31 +- .../rocketmq/dashboard/model/AclInfo.java | 209 +++++++ .../dashboard/model/PolicyRequest.java | 3 +- .../rocketmq/dashboard/model/UserInfoDto.java | 45 ++ .../model/request/UserCreateRequest.java | 3 +- .../model/request/UserUpdateRequest.java | 3 +- .../dashboard/service/AclService.java | 17 +- .../service/impl/AclServiceImpl.java | 299 +++++++--- .../service/impl/ConsumerServiceImpl.java | 25 +- .../provider/UserInfoProviderImpl.java | 5 +- .../controller/AclControllerTest.java | 221 +++---- 20 files changed, 1158 insertions(+), 613 deletions(-) create mode 100644 src/main/java/org/apache/rocketmq/dashboard/model/AclInfo.java create mode 100644 src/main/java/org/apache/rocketmq/dashboard/model/UserInfoDto.java 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) ![image-20250706145313629](UserGuide_CN/image-20250706145313629.png) @@ -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}) - }); - return await response.json(); // 返回字符串消息 - }, - - updateUser: async (brokerAddress, userInfo) => { - 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) => { + 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({brokerName, userInfo, clusterName}) + }); + return await response.json(); + }, + + 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} /> 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) => ( + + {text} + + ) + }, + {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) => ( + + {text} + + ) + }, + {title: t.SUBSCRIPTION_EXPRESSION, dataIndex: 'subString', key: 'subString', width: 150, ellipsis: true}, + { + title: t.EXPRESSION_TYPE, dataIndex: 'expressionType', key: 'expressionType', width: 120, + render: (text) => {text} + }, + // --- 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 ? ( + + {tags.map((tag, index) => ( + {tag} + ))} + + ) : '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 ? ( + + {codes.map((code, index) => ( + {code} + ))} + + ) : '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 ( {connectionData && ( <> + + + {connectionData.consumeType} + + + {connectionData.messageModel} + + + {connectionData.consumeFromWhere} + + + +

{t.CLIENT_CONNECTIONS}

-

{t.SUBSCRIPTION}

+ +

{t.CLIENT_SUBSCRIPTIONS}

({ - topic, - ...detail, - })) - : [] - } - rowKey="topic" + dataSource={formattedSubscriptionData} + rowKey="key" pagination={false} - locale={{ - emptyText: loading ? : t.NO_DATA - }} + scroll={{x: 'max-content'}} /> -

ConsumeType: {connectionData.consumeType}

-

MessageModel: {connectionData.messageModel}

)}
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) => ( + 0 ? '#f5222d' : '#52c41a'}}> + {diff} + + ) + }, + {title: 'Client Info', dataIndex: 'clientInfo', width: 200}, + { + title: 'Last Consume Time', dataIndex: 'lastTimestamp', width: 180, + render: (timestamp) => formatTimestamp(timestamp) + }, ]; return ( Consumer Details - Group: {group} | Address: {address}} visible={visible} onCancel={onCancel} footer={null} - width={1200} + width={1400} + style={{top: 20}} > - {details.map((consumeDetail, index) => ( -
-
+ {Object.entries(groupedDetails).map(([topic, topicDetails]) => ( +
+
+

Topic: {topic}

+
+ Total Lag: {topicDetails[0].diffTotal} + Last Consume Time: {formatTimestamp(topicDetails[0].lastTimestamp)} +
+
+ + {topicDetails.map((detail, index) => ( +
+
`${record.brokerName}-${record.queueId}`} + pagination={false} + size="small" + bordered + scroll={{x: 'max-content'}} + /> + + ))} ))} 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 : '********'} @@ -508,14 +489,14 @@ const Acl = () => { render: (_, record) => ( writeOperationEnabled ? ( - + handleDeleteUser(record.username)} okText={t.YES} cancelText={t.NO} > - + ) : null @@ -567,14 +548,14 @@ const Acl = () => { render: (_, record) => ( writeOperationEnabled ? ( - + handleDeleteAcl(record.subject, record.resource)} okText={t.YES} cancelText={t.NO} > - + ) : null @@ -582,233 +563,236 @@ const Acl = () => { }, ]; -return ( - <> - {msgContextHolder} -
-

{t.ACL_MANAGEMENT}

+ return ( + <> + {msgContextHolder} +
+

{t.ACL_MANAGEMENT}

-
- - - - -
- - - - - - -
- - -
- - {activeTab === 'users' && ( -
- )} - - {activeTab === 'acls' && ( -
- )} - - {/* User Management Modal */} - setIsUserModalVisible(false)} - confirmLoading={loading} - footer={[ - , - , - ]} - > -
- - - - - (visible ? : )} +
+ + - Super - Normal - - - - - - - - - {/* ACL Permission Management Modal */} - setIsAclModalVisible(false)} - confirmLoading={loading} - > -
- - - - - - - - - - {isUpdate ? ( - - ) : ( - - )} - - - - - - + + placeholder={t.PLEASE_SELECT_BROKER} + style={{width: 200}} + onChange={handleBrokerChange} + value={selectedBroker} + options={brokerNamesOptions} + disabled={!selectedCluster} + allowClear + /> - + {t.CONFIRM} + +
+ + + + + + +
+ + +
+ + {activeTab === 'users' && ( +
+ )} + + {activeTab === 'acls' && ( +
+ )} + + {/* User Management Modal */} + setIsUserModalVisible(false)} + confirmLoading={loading} + footer={[ + , + , + ]} + > + - - - - - - -); + + + + + (visible ? : )} + /> + + + + + + + + + + + {/* ACL Permission Management Modal */} + setIsAclModalVisible(false)} + confirmLoading={loading} + > +
+ + + + + + + + + + {isUpdate ? ( + + ) : ( + + )} + + + + + + + + + + + + +
+ + + ); } 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} -
+
-
+
{/* 左侧:筛选和操作按钮 */} -
-
- +
+
+ handleFilterInputChange(e.target.value)} placeholder="输入订阅组名称" @@ -504,10 +510,10 @@ const ConsumerGroupList = () => {
{/* 右侧:代理选项 */} -
- +
+