From bd94e8c4f53adc0decda96b714c8e6fefe7efa39 Mon Sep 17 00:00:00 2001 From: Crazylychee <110229037+Crazylychee@users.noreply.github.com> Date: Mon, 16 Jun 2025 13:47:34 +0800 Subject: [PATCH] [GSOC][RIP-78][ISSUES#308] Add part of refactored front-end files (#311) --- .../src/components/acl/ResourceInput.jsx | 127 +++ .../src/components/acl/SubjectInput.jsx | 101 +++ .../components/consumer/ClientInfoModal.jsx | 103 +++ .../consumer/ConsumerConfigItem.jsx | 287 +++++++ .../consumer/ConsumerConfigModal.jsx | 169 ++++ .../consumer/ConsumerDetailModal.jsx | 79 ++ .../consumer/DeleteConsumerModal.jsx | 110 +++ .../topic/ConsumerResetOffsetDialog.jsx | 76 ++ .../components/topic/ConsumerViewDialog.jsx | 96 +++ .../topic/ResetOffsetResultDialog.jsx | 65 ++ .../src/components/topic/RouterViewDialog.jsx | 111 +++ .../src/components/topic/SendResultDialog.jsx | 65 ++ .../topic/SendTopicMessageDialog.jsx | 102 +++ .../topic/SkipMessageAccumulateDialog.jsx | 70 ++ .../src/components/topic/StatsViewDialog.jsx | 66 ++ .../components/topic/TopicModifyDialog.jsx | 65 ++ .../topic/TopicSingleModifyForm.jsx | 144 ++++ frontend-new/src/pages/Producer/producer.jsx | 143 ++++ frontend-new/src/pages/Proxy/proxy.jsx | 181 +++++ frontend-new/src/pages/Topic/topic.jsx | 725 ++++++++++++++++++ 20 files changed, 2885 insertions(+) create mode 100644 frontend-new/src/components/acl/ResourceInput.jsx create mode 100644 frontend-new/src/components/acl/SubjectInput.jsx create mode 100644 frontend-new/src/components/consumer/ClientInfoModal.jsx create mode 100644 frontend-new/src/components/consumer/ConsumerConfigItem.jsx create mode 100644 frontend-new/src/components/consumer/ConsumerConfigModal.jsx create mode 100644 frontend-new/src/components/consumer/ConsumerDetailModal.jsx create mode 100644 frontend-new/src/components/consumer/DeleteConsumerModal.jsx create mode 100644 frontend-new/src/components/topic/ConsumerResetOffsetDialog.jsx create mode 100644 frontend-new/src/components/topic/ConsumerViewDialog.jsx create mode 100644 frontend-new/src/components/topic/ResetOffsetResultDialog.jsx create mode 100644 frontend-new/src/components/topic/RouterViewDialog.jsx create mode 100644 frontend-new/src/components/topic/SendResultDialog.jsx create mode 100644 frontend-new/src/components/topic/SendTopicMessageDialog.jsx create mode 100644 frontend-new/src/components/topic/SkipMessageAccumulateDialog.jsx create mode 100644 frontend-new/src/components/topic/StatsViewDialog.jsx create mode 100644 frontend-new/src/components/topic/TopicModifyDialog.jsx create mode 100644 frontend-new/src/components/topic/TopicSingleModifyForm.jsx create mode 100644 frontend-new/src/pages/Producer/producer.jsx create mode 100644 frontend-new/src/pages/Proxy/proxy.jsx create mode 100644 frontend-new/src/pages/Topic/topic.jsx diff --git a/frontend-new/src/components/acl/ResourceInput.jsx b/frontend-new/src/components/acl/ResourceInput.jsx new file mode 100644 index 0000000..280e1c3 --- /dev/null +++ b/frontend-new/src/components/acl/ResourceInput.jsx @@ -0,0 +1,127 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { Input, Select, Tag, Space } from 'antd'; +import { PlusOutlined } from '@ant-design/icons'; +import React, { useState } from 'react'; + +const { Option } = Select; + +// 资源类型枚举 +const resourceTypes = [ + { value: 0, label: 'Unknown', prefix: 'UNKNOWN' }, + { value: 1, label: 'Any', prefix: 'ANY' }, + { value: 2, label: 'Cluster', prefix: 'CLUSTER' }, + { value: 3, label: 'Namespace', prefix: 'NAMESPACE' }, + { value: 4, label: 'Topic', prefix: 'TOPIC' }, + { value: 5, label: 'Group', prefix: 'GROUP' }, +]; + +const ResourceInput = ({ value = [], onChange }) => { + // 确保 value 始终是数组 + const safeValue = Array.isArray(value) ? value : []; + + const [selectedType, setSelectedType] = useState(resourceTypes[0].prefix); // 默认选中第一个 + const [resourceName, setResourceName] = useState(''); + const [inputVisible, setInputVisible] = useState(false); + const inputRef = React.useRef(null); + + // 处理删除已添加的资源 + const handleClose = removedResource => { + const newResources = safeValue.filter(resource => resource !== removedResource); + onChange(newResources); + }; + + // 显示输入框 + const showInput = () => { + setInputVisible(true); + setTimeout(() => { + inputRef.current?.focus(); + }, 0); + }; + + // 处理资源类型选择 + const handleTypeChange = type => { + setSelectedType(type); + }; + + // 处理资源名称输入 + const handleNameChange = e => { + setResourceName(e.target.value); + }; + + // 添加资源到列表 + const handleAddResource = () => { + if (resourceName) { + const fullResource = `${selectedType}:${resourceName}`; + // 避免重复添加 + if (!safeValue.includes(fullResource)) { + onChange([...safeValue, fullResource]); + } + setResourceName(''); // 清空输入 + setInputVisible(false); // 隐藏输入框 + } + }; + + return ( + + {/* 显示已添加的资源标签 */} + {safeValue.map(resource => ( // 使用 safeValue + handleClose(resource)} + color="blue" + > + {resource} + + ))} + + {/* 新增资源输入区域 */} + {inputVisible ? ( + + + + + ) : ( + + 添加资源 + + )} + + ); +}; + +export default ResourceInput; diff --git a/frontend-new/src/components/acl/SubjectInput.jsx b/frontend-new/src/components/acl/SubjectInput.jsx new file mode 100644 index 0000000..bf9f649 --- /dev/null +++ b/frontend-new/src/components/acl/SubjectInput.jsx @@ -0,0 +1,101 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { Input, Select } from 'antd'; +import React, { useState, useEffect } from 'react'; + +const { Option } = Select; + +// Subject 类型枚举 +const subjectTypes = [ + { value: 'User', label: 'User' }, +]; + +const SubjectInput = ({ value, onChange, disabled }) => { + // 解析传入的 value,将其拆分为 type 和 name + const parseValue = (val) => { + if (!val || typeof val !== 'string') { + return { type: subjectTypes[0].value, name: '' }; // 默认值 + } + const parts = val.split(':'); + if (parts.length === 2 && subjectTypes.some(t => t.value === parts[0])) { + return { type: parts[0], name: parts[1] }; + } + return { type: subjectTypes[0].value, name: val }; // 如果格式不匹配,将整个值作为 name,类型设为默认 + }; + + const [currentType, setCurrentType] = useState(() => parseValue(value).type); + const [currentName, setCurrentName] = useState(() => parseValue(value).name); + + // 当外部 value 变化时,更新内部状态 + useEffect(() => { + const parsed = parseValue(value); + setCurrentType(parsed.type); + setCurrentName(parsed.name); + }, [value]); + + // 当类型或名称变化时,通知 Form.Item + const triggerChange = (changedType, changedName) => { + if (onChange) { + // 只有当名称不为空时才组合,否则只返回类型或空字符串 + if (changedName) { + onChange(`${changedType}:${changedName}`); + } else if (changedType) { // 如果只选择了类型,但名称为空,则不组合 + onChange(''); // 或者根据需求返回 'User:' 等,但通常这种情况下不应该有值 + } else { + onChange(''); + } + } + }; + + const onTypeChange = (newType) => { + setCurrentType(newType); + triggerChange(newType, currentName); + }; + + const onNameChange = (e) => { + const newName = e.target.value; + setCurrentName(newName); + triggerChange(currentType, newName); + }; + + return ( + + + + + ); +}; + +export default SubjectInput; diff --git a/frontend-new/src/components/consumer/ClientInfoModal.jsx b/frontend-new/src/components/consumer/ClientInfoModal.jsx new file mode 100644 index 0000000..ed4a5ca --- /dev/null +++ b/frontend-new/src/components/consumer/ClientInfoModal.jsx @@ -0,0 +1,103 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import React, { useState, useEffect } from 'react'; +import { Modal, Table, Spin } from 'antd'; +import { remoteApi } from '../../api/remoteApi/remoteApi'; +import { useLanguage } from '../../i18n/LanguageContext'; + +const ClientInfoModal = ({ visible, group, address, onCancel }) => { + const { t } = useLanguage(); + const [loading, setLoading] = useState(false); + const [connectionData, setConnectionData] = useState(null); + const [subscriptionData, setSubscriptionData] = useState(null); + + useEffect(() => { + const fetchData = async () => { + if (!visible) return; + + setLoading(true); + try { + const connResponse = await remoteApi.queryConsumerConnection(group, address); + const topicResponse = await remoteApi.queryTopicByConsumer(group, address); + + if (connResponse.status === 0) setConnectionData(connResponse.data); + if (topicResponse.status === 0) setSubscriptionData(topicResponse.data); + } finally { + setLoading(false); + } + }; + + fetchData(); + }, [visible, group, address]); + + const connectionColumns = [ + { title: 'ClientId', dataIndex: 'clientId' }, + { title: 'ClientAddr', dataIndex: 'clientAddr' }, + { title: 'Language', dataIndex: 'language' }, + { title: 'Version', dataIndex: 'versionDesc' }, + ]; + + const subscriptionColumns = [ + { title: 'Topic', dataIndex: 'topic' }, + { title: 'SubExpression', dataIndex: 'subString' }, + ]; + + return ( + + + {connectionData && ( + <> + +

{t.SUBSCRIPTION}

+
({ + topic, + ...detail, + })) + : [] + } + rowKey="topic" + pagination={false} + locale={{ + emptyText: loading ? : t.NO_DATA + }} + /> +

ConsumeType: {connectionData.consumeType}

+

MessageModel: {connectionData.messageModel}

+ + )} +
+ + ); +}; + +export default ClientInfoModal; diff --git a/frontend-new/src/components/consumer/ConsumerConfigItem.jsx b/frontend-new/src/components/consumer/ConsumerConfigItem.jsx new file mode 100644 index 0000000..cd083c6 --- /dev/null +++ b/frontend-new/src/components/consumer/ConsumerConfigItem.jsx @@ -0,0 +1,287 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import React, { useEffect, useState } from 'react'; +import { Button, Descriptions, Form, Input, Select, Switch, message } from 'antd'; +import { remoteApi } from '../../api/remoteApi/remoteApi'; // 确保路径正确 + +const { Option } = Select; + +const ConsumerConfigItem = ({ initialConfig, isAddConfig, group, brokerName, allBrokerList, allClusterNames,onCancel, onSuccess, t }) => { + const [form] = Form.useForm(); + const [currentBrokerName, setCurrentBrokerName] = useState(brokerName); + + useEffect(() => { + if (initialConfig) { + if (!isAddConfig && initialConfig.brokerNameList && initialConfig.brokerNameList.length > 0) { + // 更新模式,设置当前BrokerName为第一个(如果只有一个的话,或者您有其他选择逻辑) + setCurrentBrokerName(initialConfig.brokerNameList[0]); + } + + form.setFieldsValue({ + ...initialConfig.subscriptionGroupConfig, + groupName: isAddConfig ? undefined : initialConfig.subscriptionGroupConfig.groupName, // 添加模式下groupName可编辑 + brokerName: isAddConfig ? [] : initialConfig.brokerNameList, // 更新模式下显示已有的brokerName + clusterName: isAddConfig ? [] : initialConfig.clusterNameList, // 更新模式下显示已有的clusterName + }); + } else { + // Reset form for add mode or when initialConfig is null (e.g., when the modal is closed) + form.resetFields(); + form.setFieldsValue({ + groupName: undefined, + autoCommit: true, + enableAutoCommit: true, + enableAutoOffsetReset: true, + groupSysFlag: 0, + consumeTimeoutMinute: 10, + consumeEnable: true, + consumeMessageOrderly: false, + consumeBroadcastEnable: false, + retryQueueNums: 1, + retryMaxTimes: 16, + brokerId: 0, + whichBrokerWhenConsumeSlowly: 0, + brokerName: [], + clusterName: [], + }); + setCurrentBrokerName(undefined); // 清空当前brokerName + } + }, [initialConfig, isAddConfig, form]); + + const handleSubmit = async () => { + try { + const values = await form.validateFields(); + const numericValues = { + retryQueueNums: Number(values.retryQueueNums), + retryMaxTimes: Number(values.retryMaxTimes), + brokerId: Number(values.brokerId), + whichBrokerWhenConsumeSlowly: Number(values.whichBrokerWhenConsumeSlowly), + }; + + // 确保brokerNameList是数组 + let finalBrokerNameList = Array.isArray(values.brokerName) ? values.brokerName : [values.brokerName]; + // 确保clusterNameList是数组 + let finalClusterNameList = Array.isArray(values.clusterName) ? values.clusterName : [values.clusterName]; + + const payload = { + subscriptionGroupConfig: { + ...(initialConfig && initialConfig.subscriptionGroupConfig ? initialConfig.subscriptionGroupConfig : {}), // 保留旧的配置,除非被新值覆盖 + ...values, + ...numericValues, + groupName: isAddConfig ? values.groupName : group, // 添加模式使用表单中的groupName,更新模式使用传入的group + }, + brokerNameList: finalBrokerNameList, + clusterNameList: isAddConfig ? finalClusterNameList : null, // 更新模式保留原有clusterNameList + }; + + const response = await remoteApi.createOrUpdateConsumer(payload); + if (response.status === 0) { + message.success(t.SUCCESS); + onSuccess(); + } else { + message.error(`${t.OPERATION_FAILED}: ${response.errMsg}`); + console.error('Failed to create or update consumer:', response.errMsg); + } + } catch (error) { + console.error('Validation failed or API call error:', error); + message.error(t.FORM_VALIDATION_FAILED); + } finally { + onCancel() + } + }; + + // Helper function to parse input value to number + const parseNumber = (event) => { + const value = event.target.value; + return value === '' ? undefined : Number(value); + }; + + // 如果是添加模式,并且用户还没有选择brokerName,或者没有clusterName可供选择,则不渲染表单 + if (isAddConfig && (!allBrokerList || allBrokerList.length === 0 || !allClusterNames || allClusterNames.length === 0)) { + return

{t.NO_DATA}

; + } + + return ( +
+ {/* 标题根据当前BrokerName或“添加新配置”显示 */} +

{isAddConfig ? t.ADD_CONSUMER : `${t.CONFIG_FOR_BROKER}: ${currentBrokerName || 'N/A'}`}

+ {!isAddConfig && initialConfig && ( + + + {initialConfig.clusterNameList?.join(', ') || 'N/A'} + + +
+                            {JSON.stringify(
+                                initialConfig.subscriptionGroupConfig.groupRetryPolicy,
+                                null,
+                                2
+                            ) || 'N/A'}
+                        
+
+ + {`${initialConfig.subscriptionGroupConfig.consumeTimeoutMinute} ${t.MINUTES}` || 'N/A'} + + + {initialConfig.subscriptionGroupConfig.groupSysFlag || 'N/A'} + +
+ )} + +
+ + + + + {isAddConfig && ( + + + + )} + + + + + + + + + + + + + + + + + + +
+ Number(value) + }, { + required: true, + message: t.CANNOT_BE_EMPTY + }]} + getValueFromEvent={parseNumber} + > + + + + Number(value) + }, { + required: true, + message: t.CANNOT_BE_EMPTY + }]} + getValueFromEvent={parseNumber} + > + + + + Number(value) + }, { + required: true, + message: t.CANNOT_BE_EMPTY + }]} + getValueFromEvent={parseNumber} + > + + + + Number(value) + }, { + required: true, + message: t.CANNOT_BE_EMPTY + }]} + getValueFromEvent={parseNumber} + > + + +
+
+ +
+ +
+ ); +}; + +export default ConsumerConfigItem; + diff --git a/frontend-new/src/components/consumer/ConsumerConfigModal.jsx b/frontend-new/src/components/consumer/ConsumerConfigModal.jsx new file mode 100644 index 0000000..a58e981 --- /dev/null +++ b/frontend-new/src/components/consumer/ConsumerConfigModal.jsx @@ -0,0 +1,169 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import React, {useEffect, useState} from 'react'; +import {Button, Modal, Spin} from 'antd'; +import {remoteApi} from '../../api/remoteApi/remoteApi'; +import {useLanguage} from '../../i18n/LanguageContext'; +import ConsumerConfigItem from './ConsumerConfigItem'; // 导入子组件 + +const ConsumerConfigModal = ({visible, isAddConfig, group, onCancel, setIsAddConfig, onSuccess}) => { + const {t} = useLanguage(); + const [loading, setLoading] = useState(false); + const [allBrokerList, setAllBrokerList] = useState([]); // 存储所有可用的broker + const [allClusterNames, setAllClusterNames] = useState([]); // 存储所有可用的cluster names + const [initialConfigData, setInitialConfigData] = useState({}); // 存储按brokerName分的初始配置数据 + + useEffect(() => { + if (visible) { + const fetchInitialData = async () => { + setLoading(true); + try { + // Fetch cluster list for broker names and cluster names + if(isAddConfig) { + const clusterResponse = await remoteApi.getClusterList(); + if (clusterResponse.status === 0 && clusterResponse.data) { + const clusterInfo = clusterResponse.data.clusterInfo; + + const brokers = []; + const clusterNames = Object.keys(clusterInfo?.clusterAddrTable || {}); + + clusterNames.forEach(clusterName => { + const brokersInCluster = clusterInfo?.clusterAddrTable?.[clusterName] || []; + brokers.push(...brokersInCluster); + }); + + setAllBrokerList([...new Set(brokers)]); // 确保brokerName唯一 + setAllClusterNames(clusterNames); + + } else { + console.error('Failed to fetch cluster list:', clusterResponse.errMsg); + } + } + if (!isAddConfig) { + // Fetch existing consumer config for update mode + const consumerConfigResponse = await remoteApi.queryConsumerConfig(group); + if (consumerConfigResponse.status === 0 && consumerConfigResponse.data && consumerConfigResponse.data.length > 0) { + const configMap = {}; + consumerConfigResponse.data.forEach(config => { + // 假设每个brokerName有一个独立的配置项 + config.brokerNameList.forEach(brokerName => { + configMap[brokerName] = { + ...config, + // 确保brokerNameList和clusterNameList是数组形式,即使API返回单值 + brokerNameList: Array.isArray(config.brokerNameList) ? config.brokerNameList : [config.brokerNameList], + clusterNameList: Array.isArray(config.clusterNameList) ? config.clusterNameList : [config.clusterNameList] + }; + }); + }); + setInitialConfigData(configMap); + } else { + console.error(`Failed to fetch consumer config for group: ${group}`); + onCancel(); // Close modal if config not found + } + } else { + // For add mode, initialize with empty values and allow selecting any broker + setInitialConfigData({ + // 当isAddConfig为true时,我们只提供一个空的配置模板,用户选择broker后会创建新的配置 + // 在这里,我们将设置一个空的初始配置,供用户选择broker来创建新配置 + newConfig: { + groupName: undefined, + subscriptionGroupConfig: { + autoCommit: true, + enableAutoCommit: true, + enableAutoOffsetReset: true, + groupSysFlag: 0, + consumeTimeoutMinute: 10, + consumeEnable: true, + consumeMessageOrderly: false, + consumeBroadcastEnable: false, + retryQueueNums: 1, + retryMaxTimes: 16, + brokerId: 0, + whichBrokerWhenConsumeSlowly: 0, + }, + brokerNameList: [], + clusterNameList: [] + } + }); + } + } catch (error) { + console.error('Error in fetching initial data:', error); + } finally { + setLoading(false); + } + }; + + fetchInitialData(); + } else { + // Reset state when modal is closed + setInitialConfigData({}); + setAllBrokerList([]); + setAllClusterNames([]); + } + }, [visible, isAddConfig, group, onCancel]); + + const getBrokersToRender = () => { + if (isAddConfig) { + return ['newConfig']; + } else { + return Object.keys(initialConfigData); + } + } + + + return ( + { + onCancel(); + setIsAddConfig(false); // 确保关闭时重置添加模式 + }} + width={800} + footer={[ + , + ]} + style={{top: 20}} // 让弹窗靠上一点,方便内容滚动 + bodyStyle={{maxHeight: 'calc(100vh - 200px)', overflowY: 'auto'}} // 允许内容滚动 + > + + {getBrokersToRender().map(brokerOrKey => ( + + ))} + + + ); +}; + +export default ConsumerConfigModal; diff --git a/frontend-new/src/components/consumer/ConsumerDetailModal.jsx b/frontend-new/src/components/consumer/ConsumerDetailModal.jsx new file mode 100644 index 0000000..d4b2367 --- /dev/null +++ b/frontend-new/src/components/consumer/ConsumerDetailModal.jsx @@ -0,0 +1,79 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import React, { useState, useEffect } from 'react'; +import { Modal, Table, Spin } from 'antd'; +import { remoteApi } from '../../api/remoteApi/remoteApi'; +import { useLanguage } from '../../i18n/LanguageContext'; + +const ConsumerDetailModal = ({ visible, group, address, onCancel }) => { + const { t } = useLanguage(); + const [loading, setLoading] = useState(false); + const [details, setDetails] = useState([]); + + useEffect(() => { + const fetchData = async () => { + if (!visible) return; + + setLoading(true); + try { + const response = await remoteApi.queryTopicByConsumer(group, address); + if (response.status === 0) { + setDetails(response.data); + } + } finally { + setLoading(false); + } + }; + + fetchData(); + }, [visible, group, address]); + + const queueColumns = [ + { title: 'Broker', dataIndex: 'brokerName' }, + { title: 'Queue', dataIndex: 'queueId' }, + { title: 'BrokerOffset', dataIndex: 'brokerOffset' }, + { title: 'ConsumerOffset', dataIndex: 'consumerOffset' }, + { title: 'DiffTotal', dataIndex: 'diffTotal' }, + { title: 'LastTimestamp', dataIndex: 'lastTimestamp' }, + ]; + + return ( + + + {details.map((consumeDetail, index) => ( +
+
+ + ))} + + + ); +}; + +export default ConsumerDetailModal; diff --git a/frontend-new/src/components/consumer/DeleteConsumerModal.jsx b/frontend-new/src/components/consumer/DeleteConsumerModal.jsx new file mode 100644 index 0000000..e9e3a1b --- /dev/null +++ b/frontend-new/src/components/consumer/DeleteConsumerModal.jsx @@ -0,0 +1,110 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import React, { useState, useEffect } from 'react'; +import { Modal, Spin, Checkbox, Button, notification } from 'antd'; +import { remoteApi } from '../../api/remoteApi/remoteApi'; +import { useLanguage } from '../../i18n/LanguageContext'; + +const DeleteConsumerModal = ({ visible, group, onCancel, onSuccess }) => { + const { t } = useLanguage(); + const [brokerList, setBrokerList] = useState([]); + const [selectedBrokers, setSelectedBrokers] = useState([]); + const [loading, setLoading] = useState(false); + + // 获取Broker列表 + useEffect(() => { + const fetchBrokers = async () => { + if (!visible) return; + + setLoading(true); + try { + const response = await remoteApi.fetchBrokerNameList(group); + if (response.status === 0) { + setBrokerList(response.data); + } + } finally { + setLoading(false); + } + }; + + fetchBrokers(); + }, [visible, group]); + + // 处理删除提交 + const handleDelete = async () => { + if (selectedBrokers.length === 0) { + notification.warning({ message: t.PLEASE_SELECT_BROKER }); + return; + } + + setLoading(true); + try { + const response = await remoteApi.deleteConsumerGroup( + group, + selectedBrokers + ); + + if (response.status === 0) { + notification.success({ message: t.DELETE_SUCCESS }); + onSuccess(); + onCancel(); + } + } finally { + setLoading(false); + } + }; + + return ( + + {t.CANCEL} + , + + ]} + > + +
{t.SELECT_DELETE_BROKERS}:
+ setSelectedBrokers(values)} + > + {brokerList.map(broker => ( +
+ {broker} +
+ ))} +
+
+
+ ); +}; + +export default DeleteConsumerModal; diff --git a/frontend-new/src/components/topic/ConsumerResetOffsetDialog.jsx b/frontend-new/src/components/topic/ConsumerResetOffsetDialog.jsx new file mode 100644 index 0000000..e5056d4 --- /dev/null +++ b/frontend-new/src/components/topic/ConsumerResetOffsetDialog.jsx @@ -0,0 +1,76 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { Button, DatePicker, Form, Modal, Select } from "antd"; +import React, { useEffect, useState } from "react"; + +const ConsumerResetOffsetDialog = ({ visible, onClose, topic, allConsumerGroupList, handleResetOffset, t }) => { + const [form] = Form.useForm(); + const [selectedConsumerGroup, setSelectedConsumerGroup] = useState([]); + const [selectedTime, setSelectedTime] = useState(null); + + useEffect(() => { + if (!visible) { + setSelectedConsumerGroup([]); + setSelectedTime(null); + form.resetFields(); + } + }, [visible, form]); + + const handleResetButtonClick = () => { + handleResetOffset(selectedConsumerGroup, selectedTime ? selectedTime.valueOf() : null); + }; + + return ( + + {t.RESET} + , + , + ]} + > +
+ +
moment(text).format('YYYY-MM-DD HH:mm:ss'), + }, + ]} + rowKey="consumerGroup" + size="small" + style={{ marginBottom: '12px' }} + /> +
`${record.brokerName}-${record.queueId}-${index}`} + size="small" + /> + + )) + )} + + ); +}; + +export default ConsumerViewDialog; diff --git a/frontend-new/src/components/topic/ResetOffsetResultDialog.jsx b/frontend-new/src/components/topic/ResetOffsetResultDialog.jsx new file mode 100644 index 0000000..4984773 --- /dev/null +++ b/frontend-new/src/components/topic/ResetOffsetResultDialog.jsx @@ -0,0 +1,65 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import {Button, Modal, Table} from "antd"; +import React from "react"; + +const ResetOffsetResultDialog = ({ visible, onClose, result, t }) => { + return ( + + {t.CLOSE} + , + ]} + > + {result && Object.entries(result).map(([groupName, groupData]) => ( +
+
+ {groupData.rollbackStatsList === null ? ( +
You Should Check It Yourself
+ ) : ( +
({ key: index, item }))} + columns={[{ dataIndex: 'item', key: 'item' }]} + pagination={false} + rowKey="key" + size="small" + bordered + showHeader={false} + /> + )} + + ))} + + ); +}; + +export default ResetOffsetResultDialog; diff --git a/frontend-new/src/components/topic/RouterViewDialog.jsx b/frontend-new/src/components/topic/RouterViewDialog.jsx new file mode 100644 index 0000000..aebb899 --- /dev/null +++ b/frontend-new/src/components/topic/RouterViewDialog.jsx @@ -0,0 +1,111 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { Button, Modal, Table } from "antd"; +import React from "react"; + +const RouterViewDialog = ({ visible, onClose, topic, routeData, t }) => { + const brokerColumns = [ + { + title: 'Broker', + dataIndex: 'brokerName', + key: 'brokerName', + }, + { + title: 'Broker Addrs', + key: 'brokerAddrs', + render: (_, record) => ( +
({ key, idx: key, address: value }))} + columns={[ + { title: 'Index', dataIndex: 'idx', key: 'idx' }, + { title: 'Address', dataIndex: 'address', key: 'address' }, + ]} + pagination={false} + bordered + size="small" + /> + ), + }, + ]; + + const queueColumns = [ + { + title: t.BROKER_NAME, + dataIndex: 'brokerName', + key: 'brokerName', + }, + { + title: t.READ_QUEUE_NUMS, + dataIndex: 'readQueueNums', + key: 'readQueueNums', + }, + { + title: t.WRITE_QUEUE_NUMS, + dataIndex: 'writeQueueNums', + key: 'writeQueueNums', + }, + { + title: t.PERM, + dataIndex: 'perm', + key: 'perm', + }, + ]; + + return ( + + {t.CLOSE} + , + ]} + > +
+
+

Broker Datas:

+ {routeData?.brokerDatas?.map((item, index) => ( +
+
+ + ))} + +
+

{t.QUEUE_DATAS}:

+
+ + + + ); +}; + +export default RouterViewDialog; diff --git a/frontend-new/src/components/topic/SendResultDialog.jsx b/frontend-new/src/components/topic/SendResultDialog.jsx new file mode 100644 index 0000000..6dd3208 --- /dev/null +++ b/frontend-new/src/components/topic/SendResultDialog.jsx @@ -0,0 +1,65 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import {Button, Form, Modal, Table} from "antd"; +import React from "react"; + +const SendResultDialog = ({ visible, onClose, result, t }) => { + return ( + + {t.CLOSE} + , + ]} + > + +
({ + key: index, + label: key, + value: typeof value === 'object' ? JSON.stringify(value, null, 2) : String(value), + })) + : [] + } + columns={[ + { dataIndex: 'label', key: 'label' }, + { + dataIndex: 'value', + key: 'value', + render: (text) =>
{text}
, + }, + ]} + pagination={false} + showHeader={false} + rowKey="key" + size="small" + /> + + + ); +}; + + + +export default SendResultDialog; diff --git a/frontend-new/src/components/topic/SendTopicMessageDialog.jsx b/frontend-new/src/components/topic/SendTopicMessageDialog.jsx new file mode 100644 index 0000000..7b9a010 --- /dev/null +++ b/frontend-new/src/components/topic/SendTopicMessageDialog.jsx @@ -0,0 +1,102 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import {Button, Checkbox, Form, Input, message, Modal} from "antd"; +import React, {useEffect} from "react"; +import {remoteApi} from "../../api/remoteApi/remoteApi"; + +const SendTopicMessageDialog = ({ + visible, + onClose, + topic, + setSendResultData, + setIsSendResultModalVisible, + setIsSendTopicMessageModalVisible, + t, + }) => { + const [form] = Form.useForm(); + + useEffect(() => { + if (visible) { + form.setFieldsValue({ + topic: topic, + tag: '', + key: '', + messageBody: '', + traceEnabled: false, + }); + } else { + form.resetFields(); + } + }, [visible, topic, form]); + + const handleSendTopicMessage = async () => { + try { + const values = await form.validateFields(); // 👈 从表单获取最新值 + const result = await remoteApi.sendTopicMessage(values); // 👈 用表单数据发送 + if (result.status === 0) { + setSendResultData(result.data); + setIsSendResultModalVisible(true); + setIsSendTopicMessageModalVisible(false); + } else { + message.error(result.errMsg); + } + } catch (error) { + console.error("Error sending message:", error); + message.error("Failed to send message"); + } + }; + + return ( + + {t.COMMIT} + , + , + ]} + > +
+ + + + + + + + + + + + + + + + +
+ ); +}; + +export default SendTopicMessageDialog; diff --git a/frontend-new/src/components/topic/SkipMessageAccumulateDialog.jsx b/frontend-new/src/components/topic/SkipMessageAccumulateDialog.jsx new file mode 100644 index 0000000..cd28a6e --- /dev/null +++ b/frontend-new/src/components/topic/SkipMessageAccumulateDialog.jsx @@ -0,0 +1,70 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { Button, Form, message, Modal, Select } from "antd"; +import React, { useEffect, useState } from "react"; + +const SkipMessageAccumulateDialog = ({ visible, onClose, topic, allConsumerGroupList, handleSkipMessageAccumulate, t }) => { + const [form] = Form.useForm(); + const [selectedConsumerGroup, setSelectedConsumerGroup] = useState([]); + + useEffect(() => { + if (!visible) { + setSelectedConsumerGroup([]); + form.resetFields(); + } + }, [visible, form]); + + const handleCommit = () => { + if (!selectedConsumerGroup.length) { + message.error(t.PLEASE_SELECT_GROUP); + return; + } + handleSkipMessageAccumulate(selectedConsumerGroup); + onClose(); + }; + + return ( + + {t.COMMIT} + , + , + ]} + > +
+ +
+ + ); +}; + +export default StatsViewDialog; diff --git a/frontend-new/src/components/topic/TopicModifyDialog.jsx b/frontend-new/src/components/topic/TopicModifyDialog.jsx new file mode 100644 index 0000000..523d9f3 --- /dev/null +++ b/frontend-new/src/components/topic/TopicModifyDialog.jsx @@ -0,0 +1,65 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +// TopicModifyDialog.js +import { Button, Modal } from "antd"; +import React from "react"; +import TopicSingleModifyForm from './TopicSingleModifyForm'; + +const TopicModifyDialog = ({ + visible, + onClose, + initialData, + bIsUpdate, + writeOperationEnabled, + allClusterNameList, + allBrokerNameList, + onSubmit, + t, + }) => { + + return ( + + {t.CLOSE} + , + ]} + Style={{ maxHeight: '70vh', overflowY: 'auto' }} + > + {initialData.map((data, index) => ( + + ))} + + ); +}; + +export default TopicModifyDialog; diff --git a/frontend-new/src/components/topic/TopicSingleModifyForm.jsx b/frontend-new/src/components/topic/TopicSingleModifyForm.jsx new file mode 100644 index 0000000..b4288e4 --- /dev/null +++ b/frontend-new/src/components/topic/TopicSingleModifyForm.jsx @@ -0,0 +1,144 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +// TopicSingleModifyForm.js +import React, { useEffect } from "react"; +import {Button, Form, Input, Select, Divider, Row, Col} from "antd"; + +const TopicSingleModifyForm = ({ + initialData, + bIsUpdate, + writeOperationEnabled, + allClusterNameList, + allBrokerNameList, + onSubmit, + formIndex, + t, + }) => { + const [form] = Form.useForm(); + + useEffect(() => { + if (initialData) { + form.setFieldsValue(initialData); + } else { + form.resetFields(); + } + }, [initialData, form, formIndex]); + + const handleFormSubmit = () => { + form.validateFields() + .then(values => { + const updatedValues = { ...values }; + // 提交时,如果 clusterNameList 或 brokerNameList 为空,则填充所有可用的名称 + if(!bIsUpdate){ + if (!updatedValues.clusterNameList || updatedValues.clusterNameList.length === 0) { + updatedValues.clusterNameList = allClusterNameList; + } + if (!updatedValues.brokerNameList || updatedValues.brokerNameList.length === 0) { + updatedValues.brokerNameList = allBrokerNameList; + } + } + onSubmit(updatedValues, formIndex); // 传递 formIndex + }) + .catch(info => { + console.log('Validate Failed:', info); + }); + }; + + const messageTypeOptions = [ + { value: 'TRANSACTION', label: 'TRANSACTION' }, + { value: 'FIFO', label: 'FIFO' }, + { value: 'DELAY', label: 'DELAY' }, + { value: 'NORMAL', label: 'NORMAL' }, + ]; + + return ( +
+ {bIsUpdate && {`${t.TOPIC_CONFIG} - ${initialData.brokerNameList ? initialData.brokerNameList.join(', ') : t.UNKNOWN_BROKER}`}} + {/* 使用 Row 居中内容 */} +
{/* 表单内容占据 12 栅格宽度,并自动居中 */} + + + ({ value: name, label: name }))} + /> + + + + + + + + + + + + + + {!initialData.sysFlag && writeOperationEnabled && ( + + + + )} + + + + + ); +}; + +export default TopicSingleModifyForm; diff --git a/frontend-new/src/pages/Producer/producer.jsx b/frontend-new/src/pages/Producer/producer.jsx new file mode 100644 index 0000000..28a2d69 --- /dev/null +++ b/frontend-new/src/pages/Producer/producer.jsx @@ -0,0 +1,143 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import React, {useEffect, useState} from 'react'; +import {Button, Form, Input, message, Select, Table} from 'antd'; +import {remoteApi} from '../../api/remoteApi/remoteApi'; + +const {Option} = Select; + +const ProducerConnectionList = () => { + const [form] = Form.useForm(); + const [allTopicList, setAllTopicList] = useState([]); + const [connectionList, setConnectionList] = useState([]); + const [loading, setLoading] = useState(false); + const [messageApi, msgContextHolder] = message.useMessage(); + useEffect(() => { + const fetchTopicList = async () => { + setLoading(true); + try { + const resp = await remoteApi.queryTopic(true); + if (!resp) { + messageApi.error("Failed to fetch topic list - no response"); + return; + } + if (resp.status === 0) { + setAllTopicList(resp.data.topicList.sort()); + } else { + messageApi.error(resp.errMsg || "Failed to fetch topic list"); + } + } catch (error) { + messageApi.error("An error occurred while fetching topic list"); + console.error("Fetch error:", error); + } finally { + setLoading(false); + } + }; + fetchTopicList(); + }, []); + + const onFinish = (values) => { + setLoading(true); + const {selectedTopic, producerGroup} = values; + remoteApi.queryProducerConnection(selectedTopic, producerGroup, (resp) => { + if (resp.status === 0) { + setConnectionList(resp.data.connectionSet); + } else { + messageApi.error(resp.errMsg || "Failed to fetch producer connection list"); + } + setLoading(false); + }); + }; + + const columns = [ + { + title: 'clientId', + dataIndex: 'clientId', + key: 'clientId', + align: 'center', + }, + { + title: 'clientAddr', + dataIndex: 'clientAddr', + key: 'clientAddr', + align: 'center', + }, + { + title: 'language', + dataIndex: 'language', + key: 'language', + align: 'center', + }, + { + title: 'version', + dataIndex: 'versionDesc', + key: 'versionDesc', + align: 'center', + }, + ]; + + return ( + <> + {msgContextHolder} +
+
+ + + + + + + + + + +
+ + + + ); +}; + +export default ProducerConnectionList; diff --git a/frontend-new/src/pages/Proxy/proxy.jsx b/frontend-new/src/pages/Proxy/proxy.jsx new file mode 100644 index 0000000..ab9a359 --- /dev/null +++ b/frontend-new/src/pages/Proxy/proxy.jsx @@ -0,0 +1,181 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import React, { useState, useEffect } from 'react'; +import { Modal, Button, Select, Input, Card, Row, Col, notification, Spin } from 'antd'; +import { useLanguage } from '../../i18n/LanguageContext'; +import { remoteApi } from "../../api/remoteApi/remoteApi"; + + +const { Option } = Select; + +const ProxyManager = () => { + const { t } = useLanguage(); + + const [loading, setLoading] = useState(false); + const [proxyAddrList, setProxyAddrList] = useState([]); + const [selectedProxy, setSelectedProxy] = useState(''); + const [newProxyAddr, setNewProxyAddr] = useState(''); + const [allProxyConfig, setAllProxyConfig] = useState({}); + + const [showModal, setShowModal] = useState(false); // 控制 Modal 弹窗显示 + const [writeOperationEnabled, setWriteOperationEnabled] = useState(true); // 写操作权限,默认 true + const [notificationApi, notificationContextHolder] = notification.useNotification(); + + useEffect(() => { + const userRole = sessionStorage.getItem("userrole"); + const isWriteEnabled = userRole === null || userRole === '1'; + setWriteOperationEnabled(isWriteEnabled); + }, []); + + useEffect(() => { + setLoading(true); + remoteApi.queryProxyHomePage((resp) => { + setLoading(false); + if (resp.status === 0) { + const { proxyAddrList, currentProxyAddr } = resp.data; + setProxyAddrList(proxyAddrList || []); + setSelectedProxy(currentProxyAddr || (proxyAddrList && proxyAddrList.length > 0 ? proxyAddrList[0] : '')); + + if (currentProxyAddr) { + localStorage.setItem('proxyAddr', currentProxyAddr); + } else if (proxyAddrList && proxyAddrList.length > 0) { + localStorage.setItem('proxyAddr', proxyAddrList[0]); + } + + } else { + notificationApi.error({ message: resp.errMsg || t.FETCH_PROXY_LIST_FAILED, duration: 2 }); + } + }); + }, [t]); + + const handleSelectChange = (value) => { + setSelectedProxy(value); + localStorage.setItem('proxyAddr', value); + }; + + + const handleAddProxyAddr = () => { + if (!newProxyAddr.trim()) { + notificationApi.warning({ message: t.INPUT_PROXY_ADDR_REQUIRED || "Please input a new proxy address.", duration: 2 }); + return; + } + setLoading(true); + remoteApi.addProxyAddr(newProxyAddr.trim(), (resp) => { + setLoading(false); + if (resp.status === 0) { + if (!proxyAddrList.includes(newProxyAddr.trim())) { + setProxyAddrList(prevList => [...prevList, newProxyAddr.trim()]); + } + setNewProxyAddr(''); + notificationApi.info({ message: t.SUCCESS || "SUCCESS", duration: 2 }); + } else { + notificationApi.error({ message: resp.errMsg || t.ADD_PROXY_FAILED, duration: 2 }); + } + }); + }; + + return ( + +
+ + ProxyServerAddressList +
+ } + bordered={false} + > + +
+ + + + + {writeOperationEnabled && ( + + + + + + setNewProxyAddr(e.target.value)} + placeholder={t.INPUT_PROXY_ADDR} + /> + + + + + + )} + + + setShowModal(false)} + title={`${t.PROXY_CONFIG} [${selectedProxy}]`} + footer={ +
+ +
+ } + width={800} + bodyStyle={{ maxHeight: '60vh', overflowY: 'auto' }} + > +
+ + {Object.entries(allProxyConfig).length > 0 ? ( + Object.entries(allProxyConfig).map(([key, value]) => ( + + + + + )) + ) : ( + + + + )} + +
{key}{value}
{t.NO_CONFIG_DATA || "No configuration data available."}
+
+ + + ); +}; + +export default ProxyManager; diff --git a/frontend-new/src/pages/Topic/topic.jsx b/frontend-new/src/pages/Topic/topic.jsx new file mode 100644 index 0000000..5363a2b --- /dev/null +++ b/frontend-new/src/pages/Topic/topic.jsx @@ -0,0 +1,725 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import React, {useEffect, useState} from 'react'; +import {Button, Checkbox, Form, Input, message, Popconfirm, Space, Table} from 'antd'; +import {useLanguage} from '../../i18n/LanguageContext'; +import {remoteApi} from '../../api/remoteApi/remoteApi'; +import ResetOffsetResultDialog from "../../components/topic/ResetOffsetResultDialog"; +import SendResultDialog from "../../components/topic/SendResultDialog"; +import TopicModifyDialog from "../../components/topic/TopicModifyDialog"; +import ConsumerViewDialog from "../../components/topic/ConsumerViewDialog"; +import ConsumerResetOffsetDialog from "../../components/topic/ConsumerResetOffsetDialog"; +import SkipMessageAccumulateDialog from "../../components/topic/SkipMessageAccumulateDialog"; +import StatsViewDialog from "../../components/topic/StatsViewDialog"; +import RouterViewDialog from "../../components/topic/RouterViewDialog"; +import SendTopicMessageDialog from "../../components/topic/SendTopicMessageDialog"; + + +const DeployHistoryList = () => { + const {t} = useLanguage(); + const [filterStr, setFilterStr] = useState(''); + const [filterNormal, setFilterNormal] = useState(true); + const [filterDelay, setFilterDelay] = useState(false); + const [filterFifo, setFilterFifo] = useState(false); + const [filterTransaction, setFilterTransaction] = useState(false); + const [filterUnspecified, setFilterUnspecified] = useState(false); + const [filterRetry, setFilterRetry] = useState(false); + const [filterDLQ, setFilterDLQ] = useState(false); + const [filterSystem, setFilterSystem] = useState(false); + const [rmqVersion, setRmqVersion] = useState(true); + const [writeOperationEnabled, setWriteOperationEnabled] = useState(true); + + const [allTopicList, setAllTopicList] = useState([]); + const [allMessageTypeList, setAllMessageTypeList] = useState([]); + const [topicShowList, setTopicShowList] = useState([]); + const [loading, setLoading] = useState(false); + + // Dialog visibility states + const [isAddUpdateTopicModalVisible, setIsAddUpdateTopicModalVisible] = useState(false); + const [isResetOffsetResultModalVisible, setIsResetOffsetResultModalVisible] = useState(false); + const [isSendResultModalVisible, setIsSendResultModalVisible] = useState(false); + const [isConsumerViewModalVisible, setIsConsumerViewModalVisible] = useState(false); + const [isConsumerResetOffsetModalVisible, setIsConsumerResetOffsetModalVisible] = useState(false); + const [isSkipMessageAccumulateModalVisible, setIsSkipMessageAccumulateModalVisible] = useState(false); + const [isStatsViewModalVisible, setIsStatsViewModalVisible] = useState(false); + const [isRouterViewModalVisible, setIsRouterViewModalVisible] = useState(false); + const [isSendTopicMessageModalVisible, setIsSendTopicMessageModalVisible] = useState(false); + + // Data for dialogs + const [currentTopicForDialogs, setCurrentTopicForDialogs] = useState(''); + const [isUpdateMode, setIsUpdateMode] = useState(false); + const [resetOffsetResultData, setResetOffsetResultData] = useState(null); + const [sendResultData, setSendResultData] = useState(null); + const [consumerData, setConsumerData] = useState(null); + const [allConsumerGroupList, setAllConsumerGroupList] = useState([]); + const [statsData, setStatsData] = useState(null); + const [routeData, setRouteData] = useState(null); + const [topicModifyData, setTopicModifyData] = useState([]); + const [sendTopicMessageData, setSendTopicMessageData] = useState({ + topic: '', + tag: '', + key: '', + messageBody: '', + traceEnabled: false, + }); + const [selectedConsumerGroups, setSelectedConsumerGroups] = useState([]); + const [resetOffsetTime, setResetOffsetTime] = useState(new Date()); + + const [allClusterNameList, setAllClusterNameList] = useState([]); + const [allBrokerNameList, setAllBrokerNameList] = useState([]); + const [messageApi, msgContextHolder] = message.useMessage(); + // Pagination config + const [paginationConf, setPaginationConf] = useState({ + current: 1, + pageSize: 10, + total: 0, + }); + + useEffect(() => { + getTopicList(); + }, []); + + useEffect(() => { + filterList(paginationConf.current); + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [filterStr, filterNormal, filterDelay, filterFifo, filterTransaction, + filterUnspecified, filterRetry, filterDLQ, filterSystem, allTopicList]); + + // Close functions for Modals + const closeAddUpdateDialog = () => { + setIsAddUpdateTopicModalVisible(false); + setTopicModifyData([]); + }; + + const closeResetOffsetResultDialog = () => { + setIsResetOffsetResultModalVisible(false); + setResetOffsetResultData(null); + }; + + const closeSendResultDialog = () => { + setIsSendResultModalVisible(false); + setSendResultData(null); + }; + + const closeConsumerViewDialog = () => { + setIsConsumerViewModalVisible(false); + setConsumerData(null); + setAllConsumerGroupList([]); + }; + + const closeConsumerResetOffsetDialog = () => { + setIsConsumerResetOffsetModalVisible(false); + setSelectedConsumerGroups([]); + setResetOffsetTime(new Date()); + setAllConsumerGroupList([]); + }; + + const closeSkipMessageAccumulateDialog = () => { + setIsSkipMessageAccumulateModalVisible(false); + setSelectedConsumerGroups([]); + setAllConsumerGroupList([]); + }; + + const closeStatsViewDialog = () => { + setIsStatsViewModalVisible(false); + setStatsData(null); + }; + + const closeRouterViewDialog = () => { + setIsRouterViewModalVisible(false); + setRouteData(null); + }; + + const closeSendTopicMessageDialog = () => { + setIsSendTopicMessageModalVisible(false); + setSendTopicMessageData({topic: '', tag: '', key: '', messageBody: '', traceEnabled: false}); + }; + + const getTopicList = async () => { + setLoading(true); + try { + const result = await remoteApi.queryTopicList(); + if (result.status === 0) { + setAllTopicList(result.data.topicNameList); + setAllMessageTypeList(result.data.messageTypeList); + setPaginationConf(prev => ({ + ...prev, + total: result.data.topicNameList.length + })); + } else { + messageApi.error(result.errMsg); + } + } catch (error) { + console.error("Error fetching topic list:", error); + messageApi.error("Failed to fetch topic list"); + } finally { + setLoading(false); + } + }; + + const refreshTopicList = async () => { + setLoading(true); + try { + const result = await remoteApi.refreshTopicList(); + if (result.status === 0) { + setAllTopicList(result.data.topicNameList); + setAllMessageTypeList(result.data.messageTypeList); + setPaginationConf(prev => ({ + ...prev, + total: result.data.topicNameList.length + })); + messageApi.success(t.REFRESHING_TOPIC_LIST); + } else { + messageApi.error(result.errMsg); + } + } catch (error) { + console.error("Error refreshing topic list:", error); + messageApi.error("Failed to refresh topic list"); + } finally { + setLoading(false); + } + }; + + const filterList = (currentPage) => { + const lowExceptStr = filterStr.toLowerCase(); + const canShowList = allTopicList.filter((topic, index) => { + if (filterStr && !topic.toLowerCase().includes(lowExceptStr)) { + return false; + } + return filterByType(topic, allMessageTypeList[index]); + }); + + const perPage = paginationConf.pageSize; + const from = (currentPage - 1) * perPage; + const to = (from + perPage) > canShowList.length ? canShowList.length : from + perPage; + + setTopicShowList(canShowList.slice(from, to)); + setPaginationConf(prev => ({ + ...prev, + current: currentPage, + total: canShowList.length + })); + }; + + const filterByType = (topic, type) => { + if (filterRetry && type.includes("RETRY")) return true; + if (filterDLQ && type.includes("DLQ")) return true; + if (filterSystem && type.includes("SYSTEM")) return true; + if (rmqVersion && filterUnspecified && type.includes("UNSPECIFIED")) return true; + if (filterNormal && type.includes("NORMAL")) return true; + if (!rmqVersion && filterNormal && type.includes("UNSPECIFIED")) return true; + if (rmqVersion && filterDelay && type.includes("DELAY")) return true; + if (rmqVersion && filterFifo && type.includes("FIFO")) return true; + if (rmqVersion && filterTransaction && type.includes("TRANSACTION")) return true; + + return false; + }; + + const handleTableChange = (pagination) => { + setPaginationConf(pagination); + filterList(pagination.current); + }; + + const openAddUpdateDialog = async (topic, isSys) => { + + setCurrentTopicForDialogs(typeof topic === 'string' ? topic : (topic && topic.name) || ''); + const isUpdate = typeof topic === 'string' && !!topic; // 如果 topic 是非空字符串,则认为是更新 + + setIsUpdateMode(isUpdate); + + try { + if (isUpdate) { + // topic 已经是字符串 + const configResult = await remoteApi.getTopicConfig(topic); + if (configResult.status === 0) { + const dataToSet = Array.isArray(configResult.data) ? configResult.data : [configResult.data]; + setTopicModifyData(dataToSet.map(item => ({ + clusterNameList: [], + brokerNameList: item.brokerNameList || [], + topicName: item.topicName, + messageType: item.messageType || 'NORMAL', + writeQueueNums: item.writeQueueNums || 8, + readQueueNums: item.readQueueNums || 8, + perm: item.perm || 7, + }))); + } else { + messageApi.error(configResult.errMsg); + return; + } + } else { + setTopicModifyData([{ + clusterNameList: [], + brokerNameList: [], + topicName: '', + messageType: 'NORMAL', + writeQueueNums: 8, + readQueueNums: 8, + perm: 7, + }]); + } + } catch (error) { + console.error("Error opening add/update dialog:", error); + messageApi.error("Failed to open dialog"); + return; + } + + if(!isUpdate){ + const clusterResult = await remoteApi.getClusterList(); + if (clusterResult.status === 0) { + setAllClusterNameList(Object.keys(clusterResult.data.clusterInfo.clusterAddrTable)); + setAllBrokerNameList(Object.keys(clusterResult.data.brokerServer)); + } else { + messageApi.error(clusterResult.errMsg); + } + } + setIsAddUpdateTopicModalVisible(true); + }; + + // Post Topic Request (Add/Update) + const postTopicRequest = async (values) => { + try { + const result = await remoteApi.createOrUpdateTopic(values); + if (result.status === 0) { + messageApi.success(t.TOPIC_OPERATION_SUCCESS); + closeAddUpdateDialog(); + if(!isUpdateMode) { + refreshTopicList(); + } + } else { + messageApi.error(result.errMsg); + } + } catch (error) { + console.error("Error creating/updating topic:", error); + messageApi.error("Failed to create/update topic"); + } + }; + + // Delete Topic + const deleteTopic = async (topicToDelete) => { + try { + const result = await remoteApi.deleteTopic(topicToDelete); + if (result.status === 0) { + messageApi.success(`${t.TOPIC} [${topicToDelete}] ${t.DELETED_SUCCESSFULLY}`); + setAllTopicList(allTopicList.filter(topic => topic !== topicToDelete)); + await refreshTopicList() + } else { + messageApi.error(result.errMsg); + } + } catch (error) { + console.error("Error deleting topic:", error); + messageApi.error("Failed to delete topic"); + } + }; + + // Open Stats View Dialog + const statsView = async (topic) => { + setCurrentTopicForDialogs(topic); + try { + const result = await remoteApi.getTopicStats(topic); + if (result.status === 0) { + setStatsData(result.data); + setIsStatsViewModalVisible(true); + } else { + messageApi.error(result.errMsg); + } + } catch (error) { + console.error("Error fetching stats:", error); + messageApi.error("Failed to fetch stats"); + } + }; + + // Open Router View Dialog + const routerView = async (topic) => { + setCurrentTopicForDialogs(topic); + try { + const result = await remoteApi.getTopicRoute(topic); + if (result.status === 0) { + setRouteData(result.data); + setIsRouterViewModalVisible(true); + } else { + messageApi.error(result.errMsg); + } + } catch (error) { + console.error("Error fetching route:", error); + messageApi.error("Failed to fetch route"); + } + }; + + // Open Consumer View Dialog + const consumerView = async (topic) => { + setCurrentTopicForDialogs(topic); + try { + const result = await remoteApi.getTopicConsumers(topic); + if (result.status === 0) { + setConsumerData(result.data); + setAllConsumerGroupList(Object.keys(result.data)); + setIsConsumerViewModalVisible(true); + } else { + messageApi.error(result.errMsg); + } + } catch (error) { + console.error("Error fetching consumers:", error); + messageApi.error("Failed to fetch consumers"); + } + }; + + // Open Consumer Reset Offset Dialog + const openConsumerResetOffsetDialog = async (topic) => { + setCurrentTopicForDialogs(topic); + try { + const result = await remoteApi.getTopicConsumerGroups(topic); + if (result.status === 0) { + if (!result.data.groupList) { + messageApi.error("No consumer groups found"); + return; + } + setAllConsumerGroupList(result.data.groupList); + setIsConsumerResetOffsetModalVisible(true); + } else { + messageApi.error(result.errMsg); + } + } catch (error) { + console.error("Error fetching consumer groups:", error); + messageApi.error("Failed to fetch consumer groups"); + } + }; + + // Open Skip Message Accumulate Dialog + const openSkipMessageAccumulateDialog = async (topic) => { + setCurrentTopicForDialogs(topic); + try { + const result = await remoteApi.getTopicConsumerGroups(topic); + if (result.status === 0) { + if (!result.data.groupList) { + messageApi.error("No consumer groups found"); + return; + } + setAllConsumerGroupList(result.data.groupList); + setIsSkipMessageAccumulateModalVisible(true); + } else { + messageApi.error(result.errMsg); + } + } catch (error) { + console.error("Error fetching consumer groups:", error); + messageApi.error("Failed to fetch consumer groups"); + } + }; + + // Open Send Topic Message Dialog + const openSendTopicMessageDialog = (topic) => { + setCurrentTopicForDialogs(topic); + setSendTopicMessageData(prev => ({...prev, topic})); + setIsSendTopicMessageModalVisible(true); + }; + + const handleInputChange = (e) => { + const {name, value} = e.target; + setSendTopicMessageData(prevData => ({ + ...prevData, + [name]: value, + })); + }; + + const handleResetOffset = async (consumerGroupList, resetTime) => { + try { + const result = await remoteApi.resetConsumerOffset({ + resetTime: resetTime, // 使用传递过来的 resetTime + consumerGroupList: consumerGroupList, // 使用传递过来的 consumerGroupList + topic: currentTopicForDialogs, + force: true + }); + if (result.status === 0) { + setResetOffsetResultData(result.data); + setIsResetOffsetResultModalVisible(true); + setIsConsumerResetOffsetModalVisible(false); + } else { + messageApi.error(result.errMsg); + } + } catch (error) { + console.error("Error resetting offset:", error); + messageApi.error("Failed to reset offset"); + } + }; + + const handleSkipMessageAccumulate = async (consumerGroupListFromDialog) => { + try { + const result = await remoteApi.skipMessageAccumulate({ + resetTime: -1, + consumerGroupList: consumerGroupListFromDialog, // 使用子组件传递的 consumerGroupList + topic: currentTopicForDialogs, // 使用父组件中管理的 topic + force: true + }); + if (result.status === 0) { + setResetOffsetResultData(result.data); // 注意这里使用了 setResetOffsetResultData,确认这是你期望的 + setIsResetOffsetResultModalVisible(true); // 注意这里使用了 setIsResetOffsetResultModalVisible,确认这是你期望的 + setIsSkipMessageAccumulateModalVisible(false); + } else { + messageApi.error(result.errMsg); + } + } catch (error) { + console.error("Error skipping message accumulate:", error); + messageApi.error("Failed to skip message accumulate"); + } + }; + + const columns = [ + { + title: t.TOPIC, + dataIndex: 'topic', + key: 'topic', + align: 'center', + render: (text) => { + const sysFlag = text.startsWith('%SYS%'); + const topic = sysFlag ? text.substring(5) : text; + return {topic}; + }, + }, + { + title: t.OPERATION, + key: 'operation', + align: 'left', + render: (_, record) => { + const sysFlag = record.topic.startsWith('%SYS%'); + const topicName = sysFlag ? record.topic.substring(5) : record.topic; + return ( + + + + + + {!sysFlag && ( + + )} + {!sysFlag && writeOperationEnabled && ( + + )} + {!sysFlag && writeOperationEnabled && ( + + )} + {!sysFlag && writeOperationEnabled && ( + deleteTopic(topicName)} + okText={t.YES} + cancelText={t.NOT} + > + + + )} + + ); + }, + }, + ]; + + return ( + <> + {msgContextHolder} +
+
+
+
+ + setFilterStr(e.target.value)} + /> + + + setFilterNormal(e.target.checked)}> + {t.NORMAL} + + + {rmqVersion && ( + <> + + setFilterDelay(e.target.checked)}> + {t.DELAY} + + + + setFilterFifo(e.target.checked)}> + {t.FIFO} + + + + setFilterTransaction(e.target.checked)}> + {t.TRANSACTION} + + + + setFilterUnspecified(e.target.checked)}> + {t.UNSPECIFIED} + + + + )} + + setFilterRetry(e.target.checked)}> + {t.RETRY} + + + + setFilterDLQ(e.target.checked)}> + {t.DLQ} + + + + setFilterSystem(e.target.checked)}> + {t.SYSTEM} + + + {writeOperationEnabled && ( + + + + )} + + + +
+
+
+
+
+ ({key: index, topic}))} + columns={columns} + pagination={paginationConf} + onChange={handleTableChange} + /> + + + + + {/* Modals/Dialogs - 传递 visible 和 onClose prop */} + + + + + + + + + + + + + + + + + + + + + ); +}; + +export default DeployHistoryList;