Copilot commented on code in PR #7851: URL: https://github.com/apache/incubator-seata/pull/7851#discussion_r2612690385
########## namingserver/src/main/java/org/apache/seata/namingserver/entity/vo/v2/NamespaceVO.java: ########## @@ -0,0 +1,34 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.seata.namingserver.entity.vo.v2; + +import java.util.HashMap; +import java.util.List; +import java.util.Map; + Review Comment: Missing documentation for the new VO class. The NamespaceVO class in the v2 package should have Javadoc documentation explaining its purpose and how it differs from the original NamespaceVO class. This is important for API consumers to understand the data structure they're receiving. ```suggestion /** * Value Object representing namespace information for the v2 API. * <p> * This class provides a mapping between cluster names and their associated vgroup lists. * It is used in the v2 version of the API and may differ from the original {@code NamespaceVO} * (in the v1 or root package) in its structure or the semantics of its fields. * <p> * <b>Differences from the original NamespaceVO:</b> * <ul> * <li>Located in the {@code org.apache.seata.namingserver.entity.vo.v2} package, indicating it is for the v2 API.</li> * <li>Contains a {@code clusterVgroups} map, which maps cluster names to lists of vgroup names.</li> * <li>May have a different structure or additional fields compared to the original version.</li> * </ul> * <p> * API consumers should refer to this class when interacting with the v2 endpoints to understand * the data structure being returned. */ ``` ########## console/src/main/resources/static/console-fe/src/utils/requestV2.ts: ########## @@ -0,0 +1,92 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +import React from 'react'; +import axios, { AxiosInstance, AxiosRequestConfig, AxiosResponse } from 'axios'; +import { Message } from '@alicloud/console-components'; +import { get } from 'lodash'; +import { GlobalStateModel } from '@/reducers'; +import { AUTHORIZATION_HEADER } from '@/contants'; + +const API_GENERAL_ERROR_MESSAGE: string = 'Request error, please try again later!'; + +const codeMessage = { + 200: '服务器成功返回请求的数据。', + 201: '新建或修改数据成功。', + 202: '一个请求已经进入后台排队(异步任务)。', + 204: '删除数据成功。', + 400: '发出的请求有错误,服务器没有进行新建或修改数据的操作。', + 401: '用户没有权限(令牌、用户名、密码错误)。', + 403: '用户得到授权,但是访问是被禁止的。', + 404: '发出的请求针对的是不存在的记录,服务器没有进行操作。', + 406: '请求的格式不可得。', + 410: '请求的资源被永久删除,且不会再得到的。', + 422: '当创建一个对象时,发生一个验证错误。', + 500: '服务器发生错误,请检查服务器。', + 502: '网关错误。', + 503: '服务不可用,服务器暂时过载或维护。', + 504: '网关超时。', + '-1000': '项目名称已存在, 请使用其他名称', +}; + +const requestV2 = () => { + const instance: AxiosInstance = axios.create({ + baseURL: 'api/v2', + method: 'get', + }); + + instance.interceptors.request.use((config: AxiosRequestConfig) => { + let authHeader: string | null = localStorage.getItem(AUTHORIZATION_HEADER); + // add jwt header + config.headers[AUTHORIZATION_HEADER] = authHeader; + + return config; + }) + + instance.interceptors.response.use( + (response: AxiosResponse): Promise<any> => { + const code = get(response, 'data.code'); + if (response.status === 200 && code === '200') { + return Promise.resolve(get(response, 'data')); + } else { + const errorText = + (codeMessage as any)[code] || + get(response, 'data.message') || + get(response, 'data.errorMsg') || + response.statusText; + Message.error(errorText || `请求错误 ${code}: ${get(response, 'config.url', '')}`); Review Comment: The error message is partially in Chinese. For consistency with the rest of the codebase and better international usability, error messages should be in English or properly internationalized. ########## namingserver/src/test/java/org/apache/seata/namingserver/NamingControllerV2Test.java: ########## @@ -0,0 +1,83 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.seata.namingserver; + +import org.apache.seata.common.metadata.Node; +import org.apache.seata.common.metadata.namingserver.NamingServerNode; +import org.apache.seata.common.result.SingleResult; +import org.apache.seata.namingserver.controller.NamingControllerV2; +import org.apache.seata.namingserver.entity.vo.v2.NamespaceVO; +import org.apache.seata.namingserver.manager.NamingManager; +import org.junit.jupiter.api.Test; +import org.junit.runner.RunWith; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.test.context.junit4.SpringRunner; + +import java.util.Map; +import java.util.UUID; + +import static org.apache.seata.common.NamingServerConstants.CONSTANT_GROUP; +import static org.junit.jupiter.api.Assertions.*; + +@RunWith(SpringRunner.class) +@SpringBootTest Review Comment: The test class is using @RunWith(SpringRunner.class) which is from JUnit 4, but it's also using @Test from JUnit 5 (org.junit.jupiter.api.Test). This mixing of JUnit 4 and JUnit 5 annotations is inconsistent and could lead to unexpected behavior. Either use @ExtendWith(SpringExtension.class) from JUnit 5 or use org.junit.Test from JUnit 4 for consistency. ########## namingserver/src/main/java/org/apache/seata/namingserver/controller/NamingControllerV2.java: ########## @@ -0,0 +1,44 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.seata.namingserver.controller; + +import org.apache.seata.common.result.SingleResult; +import org.apache.seata.namingserver.entity.vo.v2.NamespaceVO; +import org.apache.seata.namingserver.manager.NamingManager; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; + +import javax.annotation.Resource; +import java.util.Map; + +@RestController +@RequestMapping(value = {"/naming/v2", "/api/v2/naming"}) +public class NamingControllerV2 { + + private static final Logger LOGGER = LoggerFactory.getLogger(NamingControllerV2.class); + + @Resource + private NamingManager namingManager; + Review Comment: Missing documentation for the new public controller method. The namespaces method in NamingControllerV2 should have Javadoc documentation explaining its purpose, the API endpoint it exposes, and what data structure it returns. This is particularly important for REST API endpoints that external consumers will use. ```suggestion /** * Retrieves all namespaces. * <p> * API Endpoint: GET /naming/v2/namespace or /api/v2/naming/namespace * </p> * * @return a {@link SingleResult} containing a map where the key is the namespace name and the value is a {@link NamespaceVO} object */ ``` ########## namingserver/src/main/java/org/apache/seata/namingserver/manager/NamingManager.java: ########## @@ -495,6 +495,64 @@ public SingleResult<Map<String, NamespaceVO>> namespace() { namespaceVO.getClusters().add(clusterName); namespaceVO.getVgroups().add(vGroup); }))); + + return SingleResult.success(namespaceVOs); + } + Review Comment: Missing documentation for the new public method. The namespaceV2 method should have Javadoc documentation explaining its purpose, return value, and how it differs from the existing namespace() method. This is especially important for public API methods to help other developers understand the API version differences. ```suggestion /** * Returns a map of namespace names to their corresponding v2 {@code NamespaceVO} representations. * <p> * This method provides a new version (v2) of the namespace information, including detailed mappings * of clusters and vgroups for each namespace. The returned structure is designed to support enhanced * querying and display of namespace relationships, such as which clusters and vgroups belong to each namespace, * and the mapping between clusters and their vgroups. * </p> * <p> * <b>Difference from {@link #namespace()}:</b> Unlike the {@code namespace()} method, which returns a map of * namespaces to a basic {@code NamespaceVO} (with lists of clusters and vgroups), this v2 method returns a map * to a v2 {@code NamespaceVO} that contains more granular information, including a mapping of clusters to their * associated vgroups within each namespace. This allows clients to better understand the relationships between * namespaces, clusters, and vgroups. * </p> * * @return a {@code SingleResult} containing a map where the key is the namespace name and the value is a v2 * {@code NamespaceVO} object with detailed cluster and vgroup information. */ ``` ########## console/src/main/resources/static/console-fe/src/service/globalLockInfo.ts: ########## @@ -33,7 +34,7 @@ export type GlobalLockParam = { }; export async function fetchNamespace():Promise<any> { - const result = await request.get('/naming/namespace', { + const result = await requestV2.get('/namespace', { Review Comment: The API endpoint path is inconsistent with the backend controller mapping. The backend NamingControllerV2 is mapped to "/naming/v2/namespace" or "/api/v2/naming/namespace", but this code is calling "/namespace" which would resolve to "api/v2/namespace". This path doesn't match the backend mapping and the request will fail with a 404 error. ```suggestion const result = await requestV2.get('/naming/v2/namespace', { ``` ########## namingserver/src/main/java/org/apache/seata/namingserver/manager/NamingManager.java: ########## @@ -495,6 +495,64 @@ public SingleResult<Map<String, NamespaceVO>> namespace() { namespaceVO.getClusters().add(clusterName); namespaceVO.getVgroups().add(vGroup); }))); + + return SingleResult.success(namespaceVOs); + } + + public SingleResult<Map<String, org.apache.seata.namingserver.entity.vo.v2.NamespaceVO>> namespaceV2() { + Map<String, Set<String>> clustersMap = new HashMap<>(); + Map<String, Set<String>> vgroupsMap = new HashMap<>(); + Map<String, Map<String, Set<String>>> clusterVgroupsMap = new HashMap<>(); // namespace -> cluster -> vgroups + + // Collect all namespaces + Set<String> allNamespaces = new HashSet<>(); + allNamespaces.addAll(namespaceClusterDataMap.keySet()); + vGroupMap.asMap().values().forEach(namespaceMap -> allNamespaces.addAll(namespaceMap.keySet())); + + // Initialize maps for all namespaces + allNamespaces.forEach(namespace -> { + clustersMap.computeIfAbsent(namespace, k -> new HashSet<>()); + vgroupsMap.computeIfAbsent(namespace, k -> new HashSet<>()); + clusterVgroupsMap.computeIfAbsent(namespace, k -> new HashMap<>()); + }); + + // Collect all clusters from namespaceClusterDataMap + namespaceClusterDataMap.forEach((namespace, clusterDataMap) -> { + Set<String> clusters = clustersMap.get(namespace); + clusters.addAll(clusterDataMap.keySet()); + }); + + // Collect vgroups and build cluster to vgroups mapping + Map<String /* VGroup */, ConcurrentMap<String /* namespace */, NamespaceBO>> currentVGroupMap = + new HashMap<>(vGroupMap.asMap()); + currentVGroupMap.forEach((vGroup, namespaceMap) -> namespaceMap.forEach((namespace, namespaceBO) -> { + Set<String> vgroups = vgroupsMap.get(namespace); + vgroups.add(vGroup); + + // Build cluster to vgroups + Map<String, Set<String>> clusterVg = clusterVgroupsMap.get(namespace); + namespaceBO.getClusterMap().forEach((clusterName, clusterBO) -> { + Set<String> vgSet = clusterVg.computeIfAbsent(clusterName, k -> new HashSet<>()); + vgSet.add(vGroup); + }); + })); + + // Build NamespaceVOv2 Review Comment: Typo in comment. "NamespaceVOv2" should be "NamespaceVO v2" (with space) for consistency with the class name org.apache.seata.namingserver.entity.vo.v2.NamespaceVO. ```suggestion // Build NamespaceVO v2 ``` ########## console/src/main/resources/static/console-fe/src/pages/TransactionInfo/TransactionInfo.tsx: ########## @@ -949,6 +1016,12 @@ class TransactionInfo extends React.Component<GlobalProps, TransactionInfoState> <Icon type="search" />{searchButtonLabel} </Form.Submit> </FormItem> + {/* {create vgroup button} */} Review Comment: The comment syntax is incorrect for JSX/TSX. This should use standard single-line comment syntax or be removed entirely. HTML-style comments are not valid in JSX and this may cause parsing issues. ########## console/src/main/resources/static/console-fe/src/pages/GlobalLockInfo/GlobalLockInfo.tsx: ########## @@ -187,15 +192,33 @@ class GlobalLockInfo extends React.Component<GlobalProps, GlobalLockInfoState> { searchFilterOnChange = (key:string, val:string) => { if (key === 'namespace') { const selectedNamespace = this.state.namespaceOptions.get(val); + const clusters = selectedNamespace ? selectedNamespace.clusters : []; + const firstCluster = clusters.length > 0 ? clusters[0] : undefined; + const clusterVgroups = selectedNamespace ? selectedNamespace.clusterVgroups : {}; + const vgroups = firstCluster ? clusterVgroups[firstCluster] || [] : []; this.setState({ - clusters: selectedNamespace ? selectedNamespace.clusters : [], - vgroups: selectedNamespace ? selectedNamespace.vgroups : [], - globalLockParam: Object.assign(this.state.globalLockParam, {[key]: val}), + clusters, + vgroups, + globalLockParam: Object.assign(this.state.globalLockParam, {[key]: val, cluster: firstCluster}), }); Review Comment: Component state update uses [potentially inconsistent value](1). ########## console/src/main/resources/static/console-fe/src/pages/TransactionInfo/TransactionInfo.tsx: ########## @@ -416,11 +425,30 @@ class TransactionInfo extends React.Component<GlobalProps, TransactionInfoState> searchFilterOnChange = (key: string, val: string) => { if (key === 'namespace') { const selectedNamespace = this.state.namespaceOptions.get(val); + const clusters = selectedNamespace ? selectedNamespace.clusters : []; + const firstCluster = clusters.length > 0 ? clusters[0] : undefined; + const clusterVgroups = selectedNamespace ? selectedNamespace.clusterVgroups : {}; + const vgroups = firstCluster ? clusterVgroups[firstCluster] || [] : []; this.setState({ - clusters: selectedNamespace ? selectedNamespace.clusters : [], - vgroups: selectedNamespace ? selectedNamespace.vgroups : [], - globalSessionParam: Object.assign(this.state.globalSessionParam, {[key]: val}), + clusters, + vgroups, + globalSessionParam: Object.assign(this.state.globalSessionParam, {[key]: val, cluster: firstCluster}), }); Review Comment: Component state update uses [potentially inconsistent value](1). ########## console/src/main/resources/static/console-fe/src/pages/TransactionInfo/TransactionInfo.tsx: ########## @@ -416,11 +425,30 @@ class TransactionInfo extends React.Component<GlobalProps, TransactionInfoState> searchFilterOnChange = (key: string, val: string) => { if (key === 'namespace') { const selectedNamespace = this.state.namespaceOptions.get(val); + const clusters = selectedNamespace ? selectedNamespace.clusters : []; + const firstCluster = clusters.length > 0 ? clusters[0] : undefined; + const clusterVgroups = selectedNamespace ? selectedNamespace.clusterVgroups : {}; + const vgroups = firstCluster ? clusterVgroups[firstCluster] || [] : []; this.setState({ - clusters: selectedNamespace ? selectedNamespace.clusters : [], - vgroups: selectedNamespace ? selectedNamespace.vgroups : [], - globalSessionParam: Object.assign(this.state.globalSessionParam, {[key]: val}), + clusters, + vgroups, + globalSessionParam: Object.assign(this.state.globalSessionParam, {[key]: val, cluster: firstCluster}), }); + } else if (key === 'cluster') { + const currentNamespace = this.state.globalSessionParam.namespace; + if (currentNamespace) { + const namespaceData = this.state.namespaceOptions.get(currentNamespace); + const clusterVgroups = namespaceData ? namespaceData.clusterVgroups : {}; + const selectedVgroups = clusterVgroups[val] || []; + this.setState({ + vgroups: selectedVgroups, + globalSessionParam: Object.assign(this.state.globalSessionParam, {[key]: val}), + }); + } else { + this.setState({ + globalSessionParam: Object.assign(this.state.globalSessionParam, {[key]: val}), + }); Review Comment: Component state update uses [potentially inconsistent value](1). ########## console/src/main/resources/static/console-fe/src/pages/GlobalLockInfo/GlobalLockInfo.tsx: ########## @@ -187,15 +192,33 @@ class GlobalLockInfo extends React.Component<GlobalProps, GlobalLockInfoState> { searchFilterOnChange = (key:string, val:string) => { if (key === 'namespace') { const selectedNamespace = this.state.namespaceOptions.get(val); + const clusters = selectedNamespace ? selectedNamespace.clusters : []; + const firstCluster = clusters.length > 0 ? clusters[0] : undefined; + const clusterVgroups = selectedNamespace ? selectedNamespace.clusterVgroups : {}; + const vgroups = firstCluster ? clusterVgroups[firstCluster] || [] : []; this.setState({ - clusters: selectedNamespace ? selectedNamespace.clusters : [], - vgroups: selectedNamespace ? selectedNamespace.vgroups : [], - globalLockParam: Object.assign(this.state.globalLockParam, {[key]: val}), + clusters, + vgroups, + globalLockParam: Object.assign(this.state.globalLockParam, {[key]: val, cluster: firstCluster}), }); + } else if (key === 'cluster') { + const currentNamespace = this.state.globalLockParam.namespace; + if (currentNamespace) { + const namespaceData = this.state.namespaceOptions.get(currentNamespace); + const clusterVgroups = namespaceData ? namespaceData.clusterVgroups : {}; + const selectedVgroups = clusterVgroups[val] || []; + this.setState({ + vgroups: selectedVgroups, + globalLockParam: Object.assign(this.state.globalLockParam, {[key]: val}), + }); + } else { + this.setState({ + globalLockParam: Object.assign(this.state.globalLockParam, {[key]: val}), + }); + } } else { this.setState({ - globalLockParam: Object.assign(this.state.globalLockParam, - {[key]: val}), + globalLockParam: Object.assign(this.state.globalLockParam, {[key]: val}), }); Review Comment: Component state update uses [potentially inconsistent value](1). ########## server/pom.xml: ########## @@ -301,7 +301,6 @@ <dependency> <groupId>com.squareup.okhttp3</groupId> <artifactId>okhttp</artifactId> Review Comment: The okhttp dependency scope has been changed from test to compile without any explanation or visible usage in the server module's main code. This change increases the production dependency footprint. Please verify that okhttp is actually needed in the main server code, or if this change was made in error and should remain as a test dependency. ```suggestion <artifactId>okhttp</artifactId> <scope>test</scope> ``` ########## console/src/main/resources/static/console-fe/src/pages/TransactionInfo/TransactionInfo.tsx: ########## @@ -839,6 +867,41 @@ class TransactionInfo extends React.Component<GlobalProps, TransactionInfoState> }); } + showCreateVGroupDialog = () => { + this.setState({ + createVGroupDialogVisible: true, + vGroupName: '', + }); + } + + closeCreateVGroupDialog = () => { + this.setState({ + createVGroupDialogVisible: false, + vGroupName: '', + }); + } + + handleCreateVGroup = () => { + const { locale = {} } = this.props; + const { createVGroupErrorMessage, createVGroupSuccessMessage, createVGroupFailMessage } = locale; + const { namespace, cluster } = this.state.globalSessionParam; + const { vGroupName } = this.state; + if (!namespace || !cluster || !vGroupName.trim()) { + Message.error(createVGroupErrorMessage); + return; + } + addGroup(namespace, cluster, vGroupName.trim()).then(() => { + Message.success(createVGroupSuccessMessage); + this.closeCreateVGroupDialog(); + // Delay 5 seconds before reloading namespaces to get the latest vgroup list + setTimeout(() => { + this.loadNamespaces(); + }, 5000); Review Comment: The hardcoded 5-second delay is a code smell. This arbitrary delay for reloading namespaces creates a poor user experience and doesn't guarantee the backend has finished processing. Consider implementing a proper callback mechanism, polling with a reasonable timeout, or returning an updated state from the API. ########## console/src/main/resources/static/console-fe/src/utils/requestV2.ts: ########## @@ -0,0 +1,92 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +import React from 'react'; +import axios, { AxiosInstance, AxiosRequestConfig, AxiosResponse } from 'axios'; +import { Message } from '@alicloud/console-components'; +import { get } from 'lodash'; +import { GlobalStateModel } from '@/reducers'; Review Comment: The GlobalStateModel import is unused in this file. This import should be removed as it serves no purpose in this utility module. ```suggestion ``` ########## console/src/main/resources/static/console-fe/src/utils/requestV2.ts: ########## @@ -0,0 +1,92 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +import React from 'react'; Review Comment: The React import is unused in this file. This import should be removed as it serves no purpose in this utility module. ```suggestion ``` ########## console/src/main/resources/static/console-fe/src/pages/TransactionInfo/TransactionInfo.tsx: ########## @@ -839,6 +867,41 @@ class TransactionInfo extends React.Component<GlobalProps, TransactionInfoState> }); } + showCreateVGroupDialog = () => { + this.setState({ + createVGroupDialogVisible: true, + vGroupName: '', + }); + } + + closeCreateVGroupDialog = () => { + this.setState({ + createVGroupDialogVisible: false, + vGroupName: '', + }); + } + + handleCreateVGroup = () => { + const { locale = {} } = this.props; + const { createVGroupErrorMessage, createVGroupSuccessMessage, createVGroupFailMessage } = locale; + const { namespace, cluster } = this.state.globalSessionParam; + const { vGroupName } = this.state; + if (!namespace || !cluster || !vGroupName.trim()) { + Message.error(createVGroupErrorMessage); Review Comment: The API call is missing input validation. The vGroupName should be validated beyond just trimming whitespace, such as checking for invalid characters, maximum length, or other business rules to prevent potential issues with VGroup names. ```suggestion // Helper function to validate vGroupName validateVGroupName = (name: string): string | null => { const trimmed = name.trim(); // Example rules: 1-32 chars, alphanumeric, underscores, hyphens only if (!trimmed) { return 'VGroup name cannot be empty.'; } if (trimmed.length > 32) { return 'VGroup name must be at most 32 characters.'; } if (!/^[a-zA-Z0-9_-]+$/.test(trimmed)) { return 'VGroup name can only contain letters, numbers, underscores, and hyphens.'; } return null; } handleCreateVGroup = () => { const { locale = {} } = this.props; const { createVGroupErrorMessage, createVGroupSuccessMessage, createVGroupFailMessage } = locale; const { namespace, cluster } = this.state.globalSessionParam; const { vGroupName } = this.state; const validationError = this.validateVGroupName(vGroupName); if (!namespace || !cluster || validationError) { Message.error(validationError || createVGroupErrorMessage); ``` ########## namingserver/src/main/java/org/apache/seata/namingserver/manager/NamingManager.java: ########## @@ -495,6 +495,64 @@ public SingleResult<Map<String, NamespaceVO>> namespace() { namespaceVO.getClusters().add(clusterName); namespaceVO.getVgroups().add(vGroup); }))); + + return SingleResult.success(namespaceVOs); + } + + public SingleResult<Map<String, org.apache.seata.namingserver.entity.vo.v2.NamespaceVO>> namespaceV2() { + Map<String, Set<String>> clustersMap = new HashMap<>(); + Map<String, Set<String>> vgroupsMap = new HashMap<>(); + Map<String, Map<String, Set<String>>> clusterVgroupsMap = new HashMap<>(); // namespace -> cluster -> vgroups + + // Collect all namespaces + Set<String> allNamespaces = new HashSet<>(); + allNamespaces.addAll(namespaceClusterDataMap.keySet()); + vGroupMap.asMap().values().forEach(namespaceMap -> allNamespaces.addAll(namespaceMap.keySet())); + + // Initialize maps for all namespaces + allNamespaces.forEach(namespace -> { + clustersMap.computeIfAbsent(namespace, k -> new HashSet<>()); + vgroupsMap.computeIfAbsent(namespace, k -> new HashSet<>()); + clusterVgroupsMap.computeIfAbsent(namespace, k -> new HashMap<>()); + }); + + // Collect all clusters from namespaceClusterDataMap + namespaceClusterDataMap.forEach((namespace, clusterDataMap) -> { + Set<String> clusters = clustersMap.get(namespace); + clusters.addAll(clusterDataMap.keySet()); + }); + + // Collect vgroups and build cluster to vgroups mapping + Map<String /* VGroup */, ConcurrentMap<String /* namespace */, NamespaceBO>> currentVGroupMap = + new HashMap<>(vGroupMap.asMap()); + currentVGroupMap.forEach((vGroup, namespaceMap) -> namespaceMap.forEach((namespace, namespaceBO) -> { + Set<String> vgroups = vgroupsMap.get(namespace); + vgroups.add(vGroup); + + // Build cluster to vgroups + Map<String, Set<String>> clusterVg = clusterVgroupsMap.get(namespace); + namespaceBO.getClusterMap().forEach((clusterName, clusterBO) -> { + Set<String> vgSet = clusterVg.computeIfAbsent(clusterName, k -> new HashSet<>()); + vgSet.add(vGroup); + }); + })); + + // Build NamespaceVOv2 + Map<String, org.apache.seata.namingserver.entity.vo.v2.NamespaceVO> namespaceVOs = new HashMap<>(); + clustersMap.forEach((namespace, clusters) -> { + org.apache.seata.namingserver.entity.vo.v2.NamespaceVO namespaceVO = + new org.apache.seata.namingserver.entity.vo.v2.NamespaceVO(); + Map<String, List<String>> clusterVgList = new HashMap<>(); + Map<String, Set<String>> clusterVgSet = clusterVgroupsMap.get(namespace); + clusters.forEach(cluster -> { + Set<String> vgSet = clusterVgSet.get(cluster); + clusterVgList.put(cluster, vgSet != null ? new ArrayList<>(vgSet) : new ArrayList<>()); + }); + namespaceVO.setClusterVgroups(clusterVgList); + + namespaceVOs.put(namespace, namespaceVO); + }); + return SingleResult.success(namespaceVOs); } Review Comment: Code duplication with the namespace() method. The namespaceV2() method has significant overlap with the existing namespace() method in terms of data collection logic. Consider refactoring common logic into helper methods to reduce duplication and improve maintainability. ########## namingserver/src/main/java/org/apache/seata/namingserver/entity/vo/v2/NamespaceVO.java: ########## @@ -0,0 +1,34 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.seata.namingserver.entity.vo.v2; + +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +public class NamespaceVO { + + Map<String, List<String>> clusterVgroups = new HashMap<>(); Review Comment: The field visibility should be private. Java best practices recommend that class fields should be private with public getter/setter methods to maintain encapsulation. ```suggestion private Map<String, List<String>> clusterVgroups = new HashMap<>(); ``` ########## console/src/main/resources/static/console-fe/src/pages/GlobalLockInfo/GlobalLockInfo.tsx: ########## @@ -40,15 +40,15 @@ import moment from 'moment'; import './index.scss'; import {get} from "lodash"; import {enUsKey, getCurrentLanguage} from "@/reducers/locale"; -import {fetchNamespace} from "@/service/transactionInfo"; +import {fetchNamespaceV2} from "@/service/transactionInfo"; Review Comment: Naming inconsistency between implementations. In GlobalLockInfo, the method is calling fetchNamespaceV2 from the transactionInfo service, but in TransactionInfo component the same import uses fetchNamespace. For better maintainability and clarity, these should use consistent function names or the GlobalLockInfo should have its own properly named service function. ########## console/src/main/resources/static/console-fe/src/pages/TransactionInfo/TransactionInfo.tsx: ########## @@ -416,11 +425,30 @@ class TransactionInfo extends React.Component<GlobalProps, TransactionInfoState> searchFilterOnChange = (key: string, val: string) => { if (key === 'namespace') { const selectedNamespace = this.state.namespaceOptions.get(val); + const clusters = selectedNamespace ? selectedNamespace.clusters : []; + const firstCluster = clusters.length > 0 ? clusters[0] : undefined; + const clusterVgroups = selectedNamespace ? selectedNamespace.clusterVgroups : {}; + const vgroups = firstCluster ? clusterVgroups[firstCluster] || [] : []; this.setState({ - clusters: selectedNamespace ? selectedNamespace.clusters : [], - vgroups: selectedNamespace ? selectedNamespace.vgroups : [], - globalSessionParam: Object.assign(this.state.globalSessionParam, {[key]: val}), + clusters, + vgroups, + globalSessionParam: Object.assign(this.state.globalSessionParam, {[key]: val, cluster: firstCluster}), }); + } else if (key === 'cluster') { + const currentNamespace = this.state.globalSessionParam.namespace; + if (currentNamespace) { + const namespaceData = this.state.namespaceOptions.get(currentNamespace); + const clusterVgroups = namespaceData ? namespaceData.clusterVgroups : {}; + const selectedVgroups = clusterVgroups[val] || []; + this.setState({ + vgroups: selectedVgroups, + globalSessionParam: Object.assign(this.state.globalSessionParam, {[key]: val}), + }); Review Comment: Component state update uses [potentially inconsistent value](1). ########## console/src/main/resources/static/console-fe/src/pages/GlobalLockInfo/GlobalLockInfo.tsx: ########## @@ -187,15 +192,33 @@ class GlobalLockInfo extends React.Component<GlobalProps, GlobalLockInfoState> { searchFilterOnChange = (key:string, val:string) => { if (key === 'namespace') { const selectedNamespace = this.state.namespaceOptions.get(val); + const clusters = selectedNamespace ? selectedNamespace.clusters : []; + const firstCluster = clusters.length > 0 ? clusters[0] : undefined; + const clusterVgroups = selectedNamespace ? selectedNamespace.clusterVgroups : {}; + const vgroups = firstCluster ? clusterVgroups[firstCluster] || [] : []; this.setState({ - clusters: selectedNamespace ? selectedNamespace.clusters : [], - vgroups: selectedNamespace ? selectedNamespace.vgroups : [], - globalLockParam: Object.assign(this.state.globalLockParam, {[key]: val}), + clusters, + vgroups, + globalLockParam: Object.assign(this.state.globalLockParam, {[key]: val, cluster: firstCluster}), }); + } else if (key === 'cluster') { + const currentNamespace = this.state.globalLockParam.namespace; + if (currentNamespace) { + const namespaceData = this.state.namespaceOptions.get(currentNamespace); + const clusterVgroups = namespaceData ? namespaceData.clusterVgroups : {}; + const selectedVgroups = clusterVgroups[val] || []; + this.setState({ + vgroups: selectedVgroups, + globalLockParam: Object.assign(this.state.globalLockParam, {[key]: val}), + }); Review Comment: Component state update uses [potentially inconsistent value](1). ########## console/src/main/resources/static/console-fe/src/pages/GlobalLockInfo/GlobalLockInfo.tsx: ########## @@ -187,15 +192,33 @@ class GlobalLockInfo extends React.Component<GlobalProps, GlobalLockInfoState> { searchFilterOnChange = (key:string, val:string) => { if (key === 'namespace') { const selectedNamespace = this.state.namespaceOptions.get(val); + const clusters = selectedNamespace ? selectedNamespace.clusters : []; + const firstCluster = clusters.length > 0 ? clusters[0] : undefined; + const clusterVgroups = selectedNamespace ? selectedNamespace.clusterVgroups : {}; + const vgroups = firstCluster ? clusterVgroups[firstCluster] || [] : []; this.setState({ - clusters: selectedNamespace ? selectedNamespace.clusters : [], - vgroups: selectedNamespace ? selectedNamespace.vgroups : [], - globalLockParam: Object.assign(this.state.globalLockParam, {[key]: val}), + clusters, + vgroups, + globalLockParam: Object.assign(this.state.globalLockParam, {[key]: val, cluster: firstCluster}), }); + } else if (key === 'cluster') { + const currentNamespace = this.state.globalLockParam.namespace; + if (currentNamespace) { + const namespaceData = this.state.namespaceOptions.get(currentNamespace); + const clusterVgroups = namespaceData ? namespaceData.clusterVgroups : {}; + const selectedVgroups = clusterVgroups[val] || []; + this.setState({ + vgroups: selectedVgroups, + globalLockParam: Object.assign(this.state.globalLockParam, {[key]: val}), + }); + } else { + this.setState({ + globalLockParam: Object.assign(this.state.globalLockParam, {[key]: val}), + }); Review Comment: Component state update uses [potentially inconsistent value](1). -- This is an automated message from the Apache Git Service. To respond to the message, please log on to GitHub and use the URL above to go to the specific comment. To unsubscribe, e-mail: [email protected] For queries about this service, please contact Infrastructure at: [email protected] --------------------------------------------------------------------- To unsubscribe, e-mail: [email protected] For additional commands, e-mail: [email protected]
