Copilot commented on code in PR #7857:
URL: https://github.com/apache/incubator-seata/pull/7857#discussion_r2618025004


##########
console/src/main/resources/static/console-fe/src/locales/zh-cn.ts:
##########
@@ -88,6 +100,49 @@ const zhCn: ILocale = {
     operateTitle: '操作',
     deleteGlobalLockTitle: '删除全局锁',
   },
+  ClusterManager: {
+    title: '集群管理',
+    subTitle: '集群节点管理',
+    selectNamespaceFilerPlaceholder: '请选择命名空间',
+    selectClusterFilerPlaceholder: '请选择集群',

Review Comment:
   Typo in placeholder text. 'Filer' should be 'Filter' in 
'selectNamespaceFilerPlaceholder' and 'selectClusterFilerPlaceholder'.



##########
namingserver/src/main/java/org/apache/seata/namingserver/manager/NamingManager.java:
##########
@@ -477,24 +511,89 @@ public Result<String> changeGroup(String namespace, 
String vGroup, String cluste
     }
 
     public SingleResult<Map<String, NamespaceVO>> namespace() {
-        // namespace->cluster->vgroups
+        // Collect data using helper method
+        NamespaceData data = collectNamespaceData();
+
         Map<String, NamespaceVO> namespaceVOs = new HashMap<>();
-        Map<String /* VGroup */, ConcurrentMap<String /* namespace */, 
NamespaceBO>> currentVGourpMap =
-                new HashMap<>(vGroupMap.asMap());
-        if (currentVGourpMap.isEmpty()) {
-            namespaceClusterDataMap.forEach((namespace, clusterDataMap) -> {
+        if (data.vgroupsMap.isEmpty()) {
+            data.clustersMap.forEach((namespace, clusters) -> {
                 NamespaceVO namespaceVO = new NamespaceVO();
-                namespaceVO.setClusters(new 
ArrayList<>(clusterDataMap.keySet()));
+                namespaceVO.setClusters(new ArrayList<>(clusters));
                 namespaceVOs.put(namespace, namespaceVO);
             });
-            return SingleResult.success(namespaceVOs);
+        } else {
+            data.vgroupsMap.forEach((namespace, vgroups) -> {
+                NamespaceVO namespaceVO = 
namespaceVOs.computeIfAbsent(namespace, k -> new NamespaceVO());
+                namespaceVO.setClusters(new 
ArrayList<>(data.clustersMap.get(namespace)));
+                namespaceVO.setVgroups(new ArrayList<>(vgroups));
+            });
         }
-        currentVGourpMap.forEach((vGroup, namespaceMap) -> 
namespaceMap.forEach(
-                (namespace, namespaceBO) -> 
namespaceBO.getClusterMap().forEach((clusterName, clusterBO) -> {
-                    NamespaceVO namespaceVO = 
namespaceVOs.computeIfAbsent(namespace, value -> new NamespaceVO());
-                    namespaceVO.getClusters().add(clusterName);
-                    namespaceVO.getVgroups().add(vGroup);
-                })));
+
         return SingleResult.success(namespaceVOs);
     }

Review Comment:
   The refactored namespace() method lacks test coverage. The existing tests 
cover namespaceV2() and getClusterData(), but there are no tests for the 
updated namespace() method which uses the new collectNamespaceData() helper. 
Consider adding test cases to ensure the refactoring maintains backward 
compatibility.



##########
console/src/main/resources/static/console-fe/src/utils/request.ts:
##########
@@ -14,60 +14,40 @@
  * 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 axios, { AxiosInstance, AxiosResponse } from 'axios';
+import { Message } from '@alifd/next';
 import { get } from 'lodash';
-import { GlobalStateModel } from '@/reducers';
 import { AUTHORIZATION_HEADER } from '@/contants';
+import { getCurrentLocaleObj } from '@/reducers/locale';
 
-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 request = () => {
+const createRequest = (baseURL: string, generalErrorMessage: string = 'Request 
error, please try again later!') => {
   const instance: AxiosInstance = axios.create({
-    baseURL: 'api/v1',
+    baseURL,
     method: 'get',
   });
 
-  instance.interceptors.request.use((config: AxiosRequestConfig) => {
+  instance.interceptors.request.use((config: any) => {
     let authHeader: string | null = localStorage.getItem(AUTHORIZATION_HEADER);
     // add jwt header
-    config.headers[AUTHORIZATION_HEADER] = authHeader;
-
+    if (config.headers) {
+      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') {
+      if (response.status === 200 && code == '200') {

Review Comment:
   Use strict equality operator (===) instead of loose equality (==) to avoid 
type coercion issues. This is a best practice in TypeScript/JavaScript to 
prevent unexpected behavior.
   ```suggestion
         if (response.status === 200 && String(code) === '200') {
   ```



##########
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:
   This API endpoint change from '/naming/namespace' to '/namespace' appears to 
be inconsistent with the V2 API structure. The requestV2 is configured with 
baseURL '/api/v2', and the endpoint '/namespace' would result in 
'/api/v2/namespace', but the NamingControllerV2 defines the endpoint as 
'/naming/v2/namespace' or '/api/v2/naming/namespace'. This mismatch will cause 
the API call to fail. The endpoint should be '/naming/namespace' to match the 
controller mapping.
   ```suggestion
     const result = await requestV2.get('/naming/namespace', {
   ```



##########
console/src/main/resources/static/console-fe/src/pages/GlobalLockInfo/GlobalLockInfo.tsx:
##########
@@ -187,15 +191,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).



##########
console/src/main/resources/static/console-fe/src/pages/GlobalLockInfo/GlobalLockInfo.tsx:
##########
@@ -187,15 +191,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/GlobalLockInfo/GlobalLockInfo.tsx:
##########
@@ -187,15 +191,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).



##########
namingserver/src/main/java/org/apache/seata/namingserver/manager/NamingManager.java:
##########
@@ -477,24 +511,89 @@ public Result<String> changeGroup(String namespace, 
String vGroup, String cluste
     }
 
     public SingleResult<Map<String, NamespaceVO>> namespace() {
-        // namespace->cluster->vgroups
+        // Collect data using helper method
+        NamespaceData data = collectNamespaceData();
+
         Map<String, NamespaceVO> namespaceVOs = new HashMap<>();
-        Map<String /* VGroup */, ConcurrentMap<String /* namespace */, 
NamespaceBO>> currentVGourpMap =
-                new HashMap<>(vGroupMap.asMap());
-        if (currentVGourpMap.isEmpty()) {
-            namespaceClusterDataMap.forEach((namespace, clusterDataMap) -> {
+        if (data.vgroupsMap.isEmpty()) {
+            data.clustersMap.forEach((namespace, clusters) -> {
                 NamespaceVO namespaceVO = new NamespaceVO();
-                namespaceVO.setClusters(new 
ArrayList<>(clusterDataMap.keySet()));
+                namespaceVO.setClusters(new ArrayList<>(clusters));
                 namespaceVOs.put(namespace, namespaceVO);
             });
-            return SingleResult.success(namespaceVOs);
+        } else {
+            data.vgroupsMap.forEach((namespace, vgroups) -> {
+                NamespaceVO namespaceVO = 
namespaceVOs.computeIfAbsent(namespace, k -> new NamespaceVO());
+                namespaceVO.setClusters(new 
ArrayList<>(data.clustersMap.get(namespace)));

Review Comment:
   Potential NullPointerException when data.clustersMap.get(namespace) returns 
null. If a namespace exists in vgroupsMap but not in clustersMap, this will 
throw NPE. Add a null check or use computeIfAbsent to ensure the namespace 
exists in clustersMap.
   ```suggestion
                   List<String> clusters = data.clustersMap.get(namespace);
                   namespaceVO.setClusters(new ArrayList<>(clusters != null ? 
clusters : Collections.emptyList()));
   ```



##########
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:
   Mixing JUnit 4 and JUnit 5 annotations. Using @RunWith (JUnit 4) with @Test 
from JUnit 5 (org.junit.jupiter.api.Test) is incorrect. Either use JUnit 5's 
@ExtendWith(SpringExtension.class) or switch to JUnit 4's @Test from 
org.junit.Test. The current setup may cause the tests to not run properly.



##########
console/src/main/resources/static/console-fe/src/pages/TransactionInfo/TransactionInfo.tsx:
##########
@@ -839,6 +885,92 @@ class TransactionInfo extends React.Component<GlobalProps, 
TransactionInfoState>
     });
   }
 
+  showCreateVGroupDialog = () => {
+    this.setState({
+      createVGroupDialogVisible: true,
+      vGroupName: '',
+    });
+  }
+
+  closeCreateVGroupDialog = () => {
+    this.setState({
+      createVGroupDialogVisible: false,
+      vGroupName: '',
+    });
+  }
+
+  showChangeVGroupDialog = () => {
+    this.setState({
+      changeVGroupDialogVisible: true,
+      selectedVGroup: '',
+      targetNamespace: '',
+      targetClusters: [],
+      targetCluster: '',
+      originalNamespace: '',
+      originalClusters: [],
+      originalCluster: '',
+      originalVGroups: [],
+    });
+  }
+
+  closeChangeVGroupDialog = () => {
+    this.setState({
+      changeVGroupDialogVisible: false,
+      selectedVGroup: '',
+      targetNamespace: '',
+      targetClusters: [],
+      targetCluster: '',
+      originalNamespace: '',
+      originalClusters: [],
+      originalCluster: '',
+      originalVGroups: [],
+    });
+  }
+
+  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();
+      }, VGROUP_REFRESH_DELAY_MS);
+    }).catch((error) => {
+      const backendMessage = lodashGet(error, 'data.message');
+      const displayMessage = backendMessage ? `${createVGroupFailMessage}: 
${backendMessage}` : createVGroupFailMessage;
+      Message.error(displayMessage);
+    });
+  }
+
+  handleChangeVGroup = () => {
+    const { locale = {} } = this.props;
+    const { changeVGroupSuccessMessage, changeVGroupFailMessage } = locale;
+    const { selectedVGroup, targetNamespace, targetCluster } = this.state;
+    if (!selectedVGroup || !targetNamespace || !targetCluster) {
+      return;
+    }
+    changeGroup(targetNamespace, targetCluster, selectedVGroup, "").then(() => 
{

Review Comment:
   Hardcoded empty string for unitName parameter should be undefined or 
omitted. Passing an empty string "" may cause backend validation issues. Since 
the backend parameter is now optional (defaultValue = "", required = false), 
either pass undefined or omit the parameter entirely.
   ```suggestion
       changeGroup(targetNamespace, targetCluster, selectedVGroup).then(() => {
   ```



##########
console/src/main/resources/static/console-fe/src/locales/en-us.ts:
##########
@@ -88,6 +100,49 @@ const enUs: ILocale = {
     operateTitle: 'operate',
     deleteGlobalLockTitle: 'Delete global lock',
   },
+  ClusterManager: {
+    title: 'Cluster Manager',
+    subTitle: 'Cluster Node Management',
+    selectNamespaceFilerPlaceholder: 'Please select namespace',
+    selectClusterFilerPlaceholder: 'Please select cluster',

Review Comment:
   Typo in placeholder text. 'Filer' should be 'Filter' in 
'selectNamespaceFilerPlaceholder' and 'selectClusterFilerPlaceholder'.



##########
console/src/main/resources/static/console-fe/src/pages/ClusterManager/ClusterManager.tsx:
##########
@@ -0,0 +1,314 @@
+/*
+ * 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 { ConfigProvider, Table, Button, Form, Icon, Dialog, Input, Select, 
Message } from '@alicloud/console-components';
+import Actions from '@alicloud/console-components-actions';
+import { withRouter } from 'react-router-dom';
+import { connect } from 'react-redux';
+import Page from '@/components/Page';
+import { GlobalProps } from '@/module';
+import { fetchNamespaceV2, fetchClusterData } from '@/service/clusterManager';
+import PropTypes from 'prop-types';
+
+import './index.scss';
+
+const FormItem = Form.Item;
+
+type ClusterManagerLocale = {
+  title?: string;
+  subTitle?: string;
+  selectNamespaceFilerPlaceholder?: string;
+  selectClusterFilerPlaceholder?: string;
+  searchButtonLabel?: string;
+  unitName?: string;
+  members?: string;
+  clusterType?: string;
+  view?: string;
+  unitDialogTitle?: string;
+  control?: string;
+  transaction?: string;
+  weight?: string;
+  healthy?: string;
+  term?: string;
+  unit?: string;
+  operations?: string;
+  internal?: string;
+  version?: string;
+  metadata?: string;
+  controlEndpoint?: string;
+  transactionEndpoint?: string;
+  metadataDialogTitle?: string;
+};
+
+type ClusterManagerState = {
+  namespaceOptions: Map<string, { clusters: string[], clusterVgroups: {[key: 
string]: string[]} }>;
+  clusters: Array<string>;
+  namespace?: string;
+  cluster?: string;
+  clusterData: any; // ClusterData
+  loading: boolean;
+  unitDialogVisible: boolean;
+  selectedUnit: any; // Unit
+  selectedUnitName: string;
+  metadataDialogVisible: boolean;
+  selectedMetadata: any;
+};
+
+class ClusterManager extends React.Component<GlobalProps, ClusterManagerState> 
{
+  static displayName = 'ClusterManager';
+
+  static propTypes = {
+    locale: PropTypes.object,
+  };
+
+  state: ClusterManagerState = {
+    namespaceOptions: new Map<string, { clusters: string[], clusterVgroups: 
{[key: string]: string[]} }>(),
+    clusters: [],
+    clusterData: null,
+    loading: false,
+    unitDialogVisible: false,
+    selectedUnit: null,
+    selectedUnitName: '',
+    metadataDialogVisible: false,
+    selectedMetadata: null,
+  };
+
+  componentDidMount = () => {
+    this.loadNamespaces();
+  };
+
+  loadNamespaces = async () => {
+    try {
+      const namespaces = await fetchNamespaceV2();
+      const namespaceOptions = new Map<string, { clusters: string[], 
clusterVgroups: {[key: string]: string[]} }>();
+      Object.keys(namespaces).forEach(namespaceKey => {
+        const namespaceData = namespaces[namespaceKey];
+        const clusterVgroups: {[key: string]: string[]} = 
namespaceData.clusterVgroups || {};
+        const clusters = Object.keys(clusterVgroups);
+        namespaceOptions.set(namespaceKey, {
+          clusters,
+          clusterVgroups,
+        });
+      });
+      if (namespaceOptions.size > 0) {
+        const firstNamespace = Array.from(namespaceOptions.keys())[0];
+        const selectedNamespace = namespaceOptions.get(firstNamespace);
+        const firstCluster = selectedNamespace ? selectedNamespace.clusters[0] 
: undefined;
+        this.setState(prevState => ({
+          ...prevState,
+          namespaceOptions,
+          namespace: firstNamespace,
+          cluster: firstCluster,
+          clusters: selectedNamespace ? selectedNamespace.clusters : [],
+        }), () => {
+          this.search();
+        });
+      } else {
+        this.setState(prevState => ({
+          ...prevState,
+          namespaceOptions,
+        }));
+      }
+    } catch (error) {
+      console.error('Failed to fetch namespaces:', error);
+    }
+  };
+
+  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;
+      this.setState(prevState => ({
+        ...prevState,
+        namespace: val,
+        cluster: firstCluster,
+        clusters,
+      }));
+    } else if (key === 'cluster') {
+      this.setState(prevState => ({
+        ...prevState,
+        cluster: val,
+      }));
+    }
+  };
+
+  search = () => {
+    const { namespace, cluster } = this.state;
+    if (!namespace || !cluster) {
+      Message.error('Please select namespace and cluster');
+      return;
+    }
+    this.setState(prevState => ({
+      ...prevState,
+      loading: true,
+    }));
+    fetchClusterData(namespace, cluster).then(data => {
+      if (data.success) {
+        this.setState(prevState => ({
+          ...prevState,
+          clusterData: data.data,
+          loading: false,
+        }));
+      } else {
+        Message.error(data.message || 'Failed to fetch cluster data');
+        this.setState(prevState => ({
+          ...prevState,
+          loading: false,
+        }));
+      }
+    }).catch(err => {
+      Message.error('Failed to fetch cluster data');
+      this.setState(prevState => ({
+        ...prevState,
+        loading: false,
+      }));
+    });
+  };
+
+  showUnitDialog = (unitName: string, unit: any) => {
+    this.setState(prevState => ({
+      ...prevState,
+      unitDialogVisible: true,
+      selectedUnit: unit,
+      selectedUnitName: unitName,
+    }));
+  };
+
+  closeUnitDialog = () => {
+    this.setState(prevState => ({
+      ...prevState,
+      unitDialogVisible: false,
+      selectedUnit: null,
+      selectedUnitName: '',
+    }));
+  };
+
+  showMetadataDialog = (metadata: any) => {
+    this.setState(prevState => ({
+      ...prevState,
+      metadataDialogVisible: true,
+      selectedMetadata: metadata,
+    }));
+  };
+
+  closeMetadataDialog = () => {
+    this.setState(prevState => ({
+      ...prevState,
+      metadataDialogVisible: false,
+      selectedMetadata: null,
+    }));
+  };
+
+  render() {
+    const { locale = {} } = this.props;
+    const rawLocale = locale.ClusterManager;
+    const clusterManagerLocale: ClusterManagerLocale = typeof rawLocale === 
'object' && rawLocale !== null ? rawLocale : {};
+    const { title, subTitle, selectNamespaceFilerPlaceholder, 
selectClusterFilerPlaceholder, searchButtonLabel, unitName, members, 
clusterType, view, unitDialogTitle, control, transaction, weight, healthy, 
term, unit, operations, internal, version, metadata, controlEndpoint, 
transactionEndpoint, metadataDialogTitle } = clusterManagerLocale;
+    const unitData = this.state.clusterData ? 
Object.entries(this.state.clusterData.unitData || {}) : [];
+    return (
+      <Page
+        title={title || 'Cluster Manager'}
+        breadcrumbs={[
+          {
+            link: '/',
+            text: title || 'Cluster Manager',
+          },
+          {
+            text: subTitle || 'Manage Clusters',
+          },
+        ]}
+      >
+        {/* search form */}
+        <Form inline labelAlign="left">
+          <FormItem name="namespace" label="namespace">
+            <Select
+              hasClear
+              placeholder={selectNamespaceFilerPlaceholder || 'Select 
namespace'}
+              onChange={(value: string) => {
+                this.searchFilterOnChange('namespace', value);
+              }}
+              
dataSource={Array.from(this.state.namespaceOptions.keys()).map(key => ({ label: 
key, value: key }))}
+              value={this.state.namespace}
+            />
+          </FormItem>
+          <FormItem name="cluster" label="cluster">
+            <Select
+              hasClear
+              placeholder={selectClusterFilerPlaceholder || 'Select cluster'}
+              onChange={(value: string) => {
+                this.searchFilterOnChange('cluster', value);
+              }}
+              dataSource={this.state.clusters.map(value => ({ label: value, 
value }))}
+              value={this.state.cluster}
+            />
+          </FormItem>
+          <FormItem>
+            <Form.Submit onClick={this.search}>
+              <Icon type="search" />{searchButtonLabel || 'Search'}
+            </Form.Submit>
+          </FormItem>
+        </Form>
+        {/* unit table */}
+        <div>
+          <Table dataSource={unitData} loading={this.state.loading}>

Review Comment:
   Table column is missing a unitName column. The table displays unit data as 
an array of entries [unitName, unit], but there's no column showing the unit 
name. Line 269 only shows the member count from the unit object 
(dataIndex="1"). Consider adding a column with dataIndex="0" to display the 
unit name for better user experience.
   ```suggestion
             <Table dataSource={unitData} loading={this.state.loading}>
               <Table.Column title={unitName || 'Unit Name'} dataIndex="0" />
   ```



##########
common/src/main/java/org/apache/seata/common/metadata/Instance.java:
##########
@@ -177,6 +186,7 @@ public Instance clone() {
         instance.setTerm(term);
         instance.setTimestamp(timestamp);
         instance.setMetadata(metadata);
+        instance.setVersion(instance.getVersion());

Review Comment:
   The clone method is incorrectly setting the version. It should copy the 
version from 'this' object, not from the newly created instance (which would 
always be null). Change 'instance.getVersion()' to 'this.version' or 
'this.getVersion()'.



##########
changes/zh-cn/2.x.md:
##########
@@ -29,6 +29,8 @@
 - [[#7664](https://github.com/apache/incubator-seata/pull/7565)] 支持神通数据库的XA模式
 - [[#7675](https://github.com/apache/incubator-seata/pull/7675)] 支持Oracle批量插入
 - [[#7663](https://github.com/apache/incubator-seata/pull/7663)] 
支持java25版本的CI流水綫

Review Comment:
   Typo in Chinese text. '流水綫' should be '流水线' (simplified Chinese for 
'pipeline').
   ```suggestion
   - [[#7663](https://github.com/apache/incubator-seata/pull/7663)] 
支持java25版本的CI流水线
   ```



##########
namingserver/src/main/java/org/apache/seata/namingserver/manager/NamingManager.java:
##########
@@ -477,24 +511,89 @@ public Result<String> changeGroup(String namespace, 
String vGroup, String cluste
     }
 
     public SingleResult<Map<String, NamespaceVO>> namespace() {
-        // namespace->cluster->vgroups
+        // Collect data using helper method
+        NamespaceData data = collectNamespaceData();
+
         Map<String, NamespaceVO> namespaceVOs = new HashMap<>();
-        Map<String /* VGroup */, ConcurrentMap<String /* namespace */, 
NamespaceBO>> currentVGourpMap =
-                new HashMap<>(vGroupMap.asMap());
-        if (currentVGourpMap.isEmpty()) {
-            namespaceClusterDataMap.forEach((namespace, clusterDataMap) -> {
+        if (data.vgroupsMap.isEmpty()) {
+            data.clustersMap.forEach((namespace, clusters) -> {
                 NamespaceVO namespaceVO = new NamespaceVO();
-                namespaceVO.setClusters(new 
ArrayList<>(clusterDataMap.keySet()));
+                namespaceVO.setClusters(new ArrayList<>(clusters));
                 namespaceVOs.put(namespace, namespaceVO);
             });
-            return SingleResult.success(namespaceVOs);
+        } else {
+            data.vgroupsMap.forEach((namespace, vgroups) -> {
+                NamespaceVO namespaceVO = 
namespaceVOs.computeIfAbsent(namespace, k -> new NamespaceVO());
+                namespaceVO.setClusters(new 
ArrayList<>(data.clustersMap.get(namespace)));
+                namespaceVO.setVgroups(new ArrayList<>(vgroups));
+            });
         }
-        currentVGourpMap.forEach((vGroup, namespaceMap) -> 
namespaceMap.forEach(
-                (namespace, namespaceBO) -> 
namespaceBO.getClusterMap().forEach((clusterName, clusterBO) -> {
-                    NamespaceVO namespaceVO = 
namespaceVOs.computeIfAbsent(namespace, value -> new NamespaceVO());
-                    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() {
+        // Collect data using helper method
+        NamespaceData data = collectNamespaceData();
+
+        // Build NamespaceVOv2
+        Map<String, org.apache.seata.namingserver.entity.vo.v2.NamespaceVO> 
namespaceVOs = new HashMap<>();
+        data.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 = 
data.clusterVgroupsMap.get(namespace);

Review Comment:
   Potential NullPointerException when clusterVgSet is null. If a namespace 
exists in clustersMap but not in clusterVgroupsMap, calling 
clusterVgSet.get(cluster) on line 547 will throw NPE. Add a null check: 
'Map<String, Set<String>> clusterVgSet = data.clusterVgroupsMap.get(namespace); 
if (clusterVgSet == null) { clusterVgSet = new HashMap<>(); }'
   ```suggestion
               Map<String, Set<String>> clusterVgSet = 
data.clusterVgroupsMap.get(namespace);
               if (clusterVgSet == null) {
                   clusterVgSet = new HashMap<>();
               }
   ```



##########
console/src/main/resources/static/console-fe/src/pages/ClusterManager/ClusterManager.tsx:
##########
@@ -0,0 +1,314 @@
+/*
+ * 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 { ConfigProvider, Table, Button, Form, Icon, Dialog, Input, Select, 
Message } from '@alicloud/console-components';

Review Comment:
   Unused import Input.
   ```suggestion
   import { ConfigProvider, Table, Button, Form, Icon, Dialog, Select, Message 
} from '@alicloud/console-components';
   ```



##########
console/src/main/resources/static/console-fe/src/app.tsx:
##########
@@ -78,7 +78,7 @@ class App extends React.Component<AppPropsType, AppStateType> 
{
   get menu() {
     const { locale }: AppPropsType = this.props;
     const { MenuRouter = {} } = locale;
-    const { overview, transactionInfo, globalLockInfo, 
sagaStatemachineDesigner } = MenuRouter;
+    const { overview, transactionInfo, globalLockInfo, clusterManager, 
sagaStatemachineDesigner } = MenuRouter;

Review Comment:
   Unused variable overview.
   ```suggestion
       const { transactionInfo, globalLockInfo, clusterManager, 
sagaStatemachineDesigner } = MenuRouter;
   ```



##########
console/src/main/resources/static/console-fe/src/pages/GlobalLockInfo/GlobalLockInfo.tsx:
##########
@@ -187,15 +191,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/ClusterManager/ClusterManager.tsx:
##########
@@ -0,0 +1,314 @@
+/*
+ * 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 { ConfigProvider, Table, Button, Form, Icon, Dialog, Input, Select, 
Message } from '@alicloud/console-components';
+import Actions from '@alicloud/console-components-actions';
+import { withRouter } from 'react-router-dom';
+import { connect } from 'react-redux';
+import Page from '@/components/Page';
+import { GlobalProps } from '@/module';
+import { fetchNamespaceV2, fetchClusterData } from '@/service/clusterManager';
+import PropTypes from 'prop-types';
+
+import './index.scss';
+
+const FormItem = Form.Item;
+
+type ClusterManagerLocale = {
+  title?: string;
+  subTitle?: string;
+  selectNamespaceFilerPlaceholder?: string;
+  selectClusterFilerPlaceholder?: string;
+  searchButtonLabel?: string;
+  unitName?: string;
+  members?: string;
+  clusterType?: string;
+  view?: string;
+  unitDialogTitle?: string;
+  control?: string;
+  transaction?: string;
+  weight?: string;
+  healthy?: string;
+  term?: string;
+  unit?: string;
+  operations?: string;
+  internal?: string;
+  version?: string;
+  metadata?: string;
+  controlEndpoint?: string;
+  transactionEndpoint?: string;
+  metadataDialogTitle?: string;
+};
+
+type ClusterManagerState = {
+  namespaceOptions: Map<string, { clusters: string[], clusterVgroups: {[key: 
string]: string[]} }>;
+  clusters: Array<string>;
+  namespace?: string;
+  cluster?: string;
+  clusterData: any; // ClusterData
+  loading: boolean;
+  unitDialogVisible: boolean;
+  selectedUnit: any; // Unit
+  selectedUnitName: string;
+  metadataDialogVisible: boolean;
+  selectedMetadata: any;
+};
+
+class ClusterManager extends React.Component<GlobalProps, ClusterManagerState> 
{
+  static displayName = 'ClusterManager';
+
+  static propTypes = {
+    locale: PropTypes.object,
+  };
+
+  state: ClusterManagerState = {
+    namespaceOptions: new Map<string, { clusters: string[], clusterVgroups: 
{[key: string]: string[]} }>(),
+    clusters: [],
+    clusterData: null,
+    loading: false,
+    unitDialogVisible: false,
+    selectedUnit: null,
+    selectedUnitName: '',
+    metadataDialogVisible: false,
+    selectedMetadata: null,
+  };
+
+  componentDidMount = () => {
+    this.loadNamespaces();
+  };
+
+  loadNamespaces = async () => {
+    try {
+      const namespaces = await fetchNamespaceV2();
+      const namespaceOptions = new Map<string, { clusters: string[], 
clusterVgroups: {[key: string]: string[]} }>();
+      Object.keys(namespaces).forEach(namespaceKey => {
+        const namespaceData = namespaces[namespaceKey];
+        const clusterVgroups: {[key: string]: string[]} = 
namespaceData.clusterVgroups || {};
+        const clusters = Object.keys(clusterVgroups);
+        namespaceOptions.set(namespaceKey, {
+          clusters,
+          clusterVgroups,
+        });
+      });
+      if (namespaceOptions.size > 0) {
+        const firstNamespace = Array.from(namespaceOptions.keys())[0];
+        const selectedNamespace = namespaceOptions.get(firstNamespace);
+        const firstCluster = selectedNamespace ? selectedNamespace.clusters[0] 
: undefined;
+        this.setState(prevState => ({
+          ...prevState,
+          namespaceOptions,
+          namespace: firstNamespace,
+          cluster: firstCluster,
+          clusters: selectedNamespace ? selectedNamespace.clusters : [],
+        }), () => {
+          this.search();
+        });
+      } else {
+        this.setState(prevState => ({
+          ...prevState,
+          namespaceOptions,
+        }));
+      }
+    } catch (error) {
+      console.error('Failed to fetch namespaces:', error);
+    }
+  };
+
+  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;
+      this.setState(prevState => ({
+        ...prevState,
+        namespace: val,
+        cluster: firstCluster,
+        clusters,
+      }));
+    } else if (key === 'cluster') {
+      this.setState(prevState => ({
+        ...prevState,
+        cluster: val,
+      }));
+    }
+  };
+
+  search = () => {
+    const { namespace, cluster } = this.state;
+    if (!namespace || !cluster) {
+      Message.error('Please select namespace and cluster');
+      return;
+    }
+    this.setState(prevState => ({
+      ...prevState,
+      loading: true,
+    }));
+    fetchClusterData(namespace, cluster).then(data => {
+      if (data.success) {
+        this.setState(prevState => ({
+          ...prevState,
+          clusterData: data.data,
+          loading: false,
+        }));
+      } else {
+        Message.error(data.message || 'Failed to fetch cluster data');
+        this.setState(prevState => ({
+          ...prevState,
+          loading: false,
+        }));
+      }
+    }).catch(err => {
+      Message.error('Failed to fetch cluster data');
+      this.setState(prevState => ({
+        ...prevState,
+        loading: false,
+      }));
+    });
+  };
+
+  showUnitDialog = (unitName: string, unit: any) => {
+    this.setState(prevState => ({
+      ...prevState,
+      unitDialogVisible: true,
+      selectedUnit: unit,
+      selectedUnitName: unitName,
+    }));
+  };
+
+  closeUnitDialog = () => {
+    this.setState(prevState => ({
+      ...prevState,
+      unitDialogVisible: false,
+      selectedUnit: null,
+      selectedUnitName: '',
+    }));
+  };
+
+  showMetadataDialog = (metadata: any) => {
+    this.setState(prevState => ({
+      ...prevState,
+      metadataDialogVisible: true,
+      selectedMetadata: metadata,
+    }));
+  };
+
+  closeMetadataDialog = () => {
+    this.setState(prevState => ({
+      ...prevState,
+      metadataDialogVisible: false,
+      selectedMetadata: null,
+    }));
+  };
+
+  render() {
+    const { locale = {} } = this.props;
+    const rawLocale = locale.ClusterManager;
+    const clusterManagerLocale: ClusterManagerLocale = typeof rawLocale === 
'object' && rawLocale !== null ? rawLocale : {};
+    const { title, subTitle, selectNamespaceFilerPlaceholder, 
selectClusterFilerPlaceholder, searchButtonLabel, unitName, members, 
clusterType, view, unitDialogTitle, control, transaction, weight, healthy, 
term, unit, operations, internal, version, metadata, controlEndpoint, 
transactionEndpoint, metadataDialogTitle } = clusterManagerLocale;

Review Comment:
   Unused variable unitName.
   ```suggestion
       const { title, subTitle, selectNamespaceFilerPlaceholder, 
selectClusterFilerPlaceholder, searchButtonLabel, members, clusterType, view, 
unitDialogTitle, control, transaction, weight, healthy, term, unit, operations, 
internal, version, metadata, controlEndpoint, transactionEndpoint, 
metadataDialogTitle } = clusterManagerLocale;
   ```



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


Reply via email to