[Enhancement] ACL can add rules in clusters (#340)

* [Enhancement] ACL can add rules in clusters and fix ISSUE #297

* rollback the yml change
This commit is contained in:
Crazylychee
2025-07-08 10:46:25 +08:00
committed by GitHub
parent 87cfa3e872
commit a4e02f472f
20 changed files with 1158 additions and 613 deletions

View File

@@ -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'
});

View File

@@ -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>

View File

@@ -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>

View File

@@ -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>

View File

@@ -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"
}

View File

@@ -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>
return (
<>
{msgContextHolder}
<div style={{padding: 24}}>
<h2>{t.ACL_MANAGEMENT}</h2>
<div style={{ marginBottom: 16, display: 'flex', gap: 16 }}>
<Form.Item label={t.PLEASE_SELECT_CLUSTER} style={{ marginBottom: 0 }}>
<Select
placeholder={t.PLEASE_SELECT_CLUSTER}
style={{ width: 200 }}
onChange={handleClusterChange}
value={selectedCluster}
options={clusterNamesOptions}
/>
</Form.Item>
<Form.Item label={t.PLEASE_SELECT_BROKER} style={{ marginBottom: 0 }}>
<Select
placeholder={t.PLEASE_SELECT_BROKER}
style={{ width: 200 }}
onChange={handleBrokerChange}
value={selectedBroker}
options={brokerNamesOptions} // Now dynamically updated
disabled={!selectedCluster} // Disable broker selection if no cluster is chosen
/>
</Form.Item>
<Button type="primary" onClick={activeTab === 'users' ? fetchUsers : fetchAcls}>
{t.CONFIRM}
</Button>
</div>
<Tabs activeKey={activeTab} onChange={setActiveTab}>
<TabPane tab={t.ACL_USERS} key="users"/>
<TabPane tab={t.ACL_PERMISSIONS} key="acls"/>
</Tabs>
<div style={{marginBottom: 16, display: 'flex', justifyContent: 'space-between'}}>
<Button type="primary" onClick={activeTab === 'users' ? handleAddUser : handleAddAcl}>
{activeTab === 'users' ? t.ADD_USER : t.ADD_ACL_PERMISSION}
</Button>
<Search
placeholder={t.SEARCH_PLACEHOLDER}
allowClear
onSearch={handleSearch}
style={{width: 300}}
/>
</div>
{activeTab === 'users' && (
<Table
columns={userColumns}
dataSource={userListData}
loading={loading}
pagination={{pageSize: 10}}
rowKey="username"
/>
)}
{activeTab === 'acls' && (
<Table
columns={aclColumns}
dataSource={aclListData}
loading={loading}
pagination={{pageSize: 10}}
rowKey="key"
/>
)}
{/* User Management Modal */}
<Modal
title={currentUser ? t.EDIT_USER : t.ADD_USER}
visible={isUserModalVisible}
onOk={handleUserModalOk}
onCancel={() => setIsUserModalVisible(false)}
confirmLoading={loading}
footer={[
<Button key="cancel" onClick={() => setIsUserModalVisible(false)}>
{t.CANCEL}
</Button>,
<Button key="submit" type="primary" onClick={handleUserModalOk} loading={loading}>
{t.CONFIRM}
</Button>,
]}
>
<Form
form={userForm}
layout="vertical"
name="user_form"
initialValues={{userStatus: 'enable'}}
>
<Form.Item
name="username"
label={t.USERNAME}
rules={[{required: true, message: t.PLEASE_ENTER_USERNAME}]}
>
<Input disabled={!!currentUser}/>
</Form.Item>
<Form.Item
name="password"
label={t.PASSWORD}
rules={[{required: !currentUser, message: t.PLEASE_ENTER_PASSWORD}]}
>
<Input.Password
placeholder={t.PASSWORD}
iconRender={visible => (visible ? <EyeOutlined/> : <EyeInvisibleOutlined/>)}
<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>
</Form>
</Modal>
{/* ACL Permission Management Modal */}
<Modal
title={currentAcl ? t.EDIT_ACL_PERMISSION : t.ADD_ACL_PERMISSION}
visible={isAclModalVisible}
onOk={handleAclModalOk}
onCancel={() => setIsAclModalVisible(false)}
confirmLoading={loading}
>
<Form
form={aclForm}
layout="vertical"
name="acl_form"
>
<Form.Item
name="subject"
label={t.SUBJECT_LABEL}
rules={[{required: true, message: t.PLEASE_ENTER_SUBJECT}]}
>
<SubjectInput disabled={!!currentAcl}/>
</Form.Item>
<Form.Item
name="policyType"
label={t.POLICY_TYPE}
rules={[{required: true, message: t.PLEASE_ENTER_POLICY_TYPE}]}
>
<Select mode="single" disabled={isUpdate} placeholder="policyType" style={{width: '100%'}}>
<Select.Option value="Custom">Custom</Select.Option>
<Select.Option value="Default">Default</Select.Option>
</Select>
</Form.Item>
<Form.Item
name="resource"
label={t.RESOURCE}
rules={[{required: true, message: t.PLEASE_ADD_RESOURCE}]}
>
{isUpdate ? (
<Input disabled={isUpdate}/>
) : (
<ResourceInput/>
)}
</Form.Item>
<Form.Item
name="actions"
label={t.OPERATION_TYPE}
>
<Select mode="multiple" placeholder="action" style={{width: '100%'}}>
<Select.Option value="All">All</Select.Option>
<Select.Option value="Pub">Pub</Select.Option>
<Select.Option value="Sub">Sub</Select.Option>
<Select.Option value="Create">Create</Select.Option>
<Select.Option value="Update">Update</Select.Option>
<Select.Option value="Delete">Delete</Select.Option>
<Select.Option value="Get">Get</Select.Option>
<Select.Option value="List">List</Select.Option>
</Select>
</Form.Item>
<Form.Item
name="sourceIps"
label={t.SOURCE_IP}
rules={[
{
validator: validateIp,
},
]}
>
<Form.Item label={t.PLEASE_SELECT_BROKER} style={{marginBottom: 0}}>
<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>
placeholder={t.PLEASE_SELECT_BROKER}
style={{width: 200}}
onChange={handleBrokerChange}
value={selectedBroker}
options={brokerNamesOptions}
disabled={!selectedCluster}
allowClear
/>
</Form.Item>
<Form.Item
name="decision"
label={t.DECISION}
rules={[{required: true, message: t.PLEASE_ENTER_DECISION}]}
<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
form={userForm}
layout="vertical"
name="user_form"
initialValues={{userStatus: 'enable'}}
>
<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>
</>
);
<Form.Item
name="username"
label={t.USERNAME}
rules={[{required: true, message: t.PLEASE_ENTER_USERNAME}]}
>
<Input disabled={!!currentUser}/>
</Form.Item>
<Form.Item
name="password"
label={t.PASSWORD}
rules={[{required: !currentUser, message: t.PLEASE_ENTER_PASSWORD}]}
>
<Input.Password
placeholder={t.PASSWORD}
iconRender={visible => (visible ? <EyeOutlined/> : <EyeInvisibleOutlined/>)}
/>
</Form.Item>
<Form.Item
name="userType"
label={t.USER_TYPE}
rules={[{required: true, message: t.PLEASE_SELECT_USER_TYPE}]}
>
<Select mode="single" placeholder="Super, Normal" style={{width: '100%'}}>
<Select.Option value="Super">Super</Select.Option>
<Select.Option value="Normal">Normal</Select.Option>
</Select>
</Form.Item>
<Form.Item
name="userStatus"
label={t.USER_STATUS}
rules={[{required: true, message: t.PLEASE_SELECT_USER_STATUS}]}
>
<Select mode="single" placeholder="enable, disable" style={{width: '100%'}}>
<Select.Option value="enable">enable</Select.Option>
<Select.Option value="disable">disable</Select.Option>
</Select>
</Form.Item>
</Form>
</Modal>
{/* ACL Permission Management Modal */}
<Modal
title={currentAcl ? t.EDIT_ACL_PERMISSION : t.ADD_ACL_PERMISSION}
visible={isAclModalVisible}
onOk={handleAclModalOk}
onCancel={() => setIsAclModalVisible(false)}
confirmLoading={loading}
>
<Form
form={aclForm}
layout="vertical"
name="acl_form"
>
<Form.Item
name="subject"
label={t.SUBJECT_LABEL}
rules={[{required: true, message: t.PLEASE_ENTER_SUBJECT}]}
>
<SubjectInput disabled={!!currentAcl} t={t}/>
</Form.Item>
<Form.Item
name="policyType"
label={t.POLICY_TYPE}
rules={[{required: true, message: t.PLEASE_ENTER_POLICY_TYPE}]}
>
<Select mode="single" disabled={isUpdate} placeholder="policyType" style={{width: '100%'}}>
<Select.Option value="Custom">Custom</Select.Option>
<Select.Option value="Default">Default</Select.Option>
</Select>
</Form.Item>
<Form.Item
name="resource"
label={t.RESOURCE}
rules={[{required: true, message: t.PLEASE_ADD_RESOURCE}]}
>
{isUpdate ? (
<Input disabled={isUpdate}/>
) : (
<ResourceInput/>
)}
</Form.Item>
<Form.Item
name="actions"
label={t.OPERATION_TYPE}
>
<Select mode="multiple" placeholder="action" style={{width: '100%'}}>
<Select.Option value="All">All</Select.Option>
<Select.Option value="Pub">Pub</Select.Option>
<Select.Option value="Sub">Sub</Select.Option>
<Select.Option value="Create">Create</Select.Option>
<Select.Option value="Update">Update</Select.Option>
<Select.Option value="Delete">Delete</Select.Option>
<Select.Option value="Get">Get</Select.Option>
<Select.Option value="List">List</Select.Option>
</Select>
</Form.Item>
<Form.Item
name="sourceIps"
label={t.SOURCE_IP}
rules={[
{
validator: validateIp,
},
]}
>
<Select
mode="tags"
style={{width: '100%'}}
placeholder={t.ENTER_IP_HINT}
onChange={handleIpChange}
onDeselect={handleIpDeselect}
value={ips}
tokenSeparators={[',', ' ']}
>
<Select.Option value="192.168.1.1">192.168.1.1</Select.Option>
<Select.Option value="0.0.0.0">0.0.0.0</Select.Option>
<Select.Option value="127.0.0.1">127.0.0.1</Select.Option>
</Select>
</Form.Item>
<Form.Item
name="decision"
label={t.DECISION}
rules={[{required: true, message: t.PLEASE_ENTER_DECISION}]}
>
<Select mode="single" placeholder="Allow, Deny" style={{width: '100%'}}>
<Select.Option value="Allow">Allow</Select.Option>
<Select.Option value="Deny">Deny</Select.Option>
</Select>
</Form.Item>
</Form>
</Modal>
</div>
</>
);
}
export default Acl;

View File

@@ -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) => {

View File

@@ -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({