This is an automated email from the ASF dual-hosted git repository.

jianbin pushed a commit to branch 2.x
in repository https://gitbox.apache.org/repos/asf/incubator-seata.git


The following commit(s) were added to refs/heads/2.x by this push:
     new 906ecb2f6e feature: add support for managing transaction groups (#7851)
906ecb2f6e is described below

commit 906ecb2f6ec9e30d9ba3302d0122917b6e8dfa24
Author: funkye <[email protected]>
AuthorDate: Tue Dec 16 17:32:07 2025 +0800

    feature: add support for managing transaction groups (#7851)
---
 changes/en-us/2.x.md                               |   2 +
 changes/zh-cn/2.x.md                               |   1 +
 .../static/console-fe/src/locales/en-us.ts         |  43 ++
 .../static/console-fe/src/locales/index.d.ts       |   1 +
 .../static/console-fe/src/locales/zh-cn.ts         |  43 ++
 .../src/pages/GlobalLockInfo/GlobalLockInfo.tsx    | 162 ++++---
 .../src/pages/TransactionInfo/TransactionInfo.tsx  | 468 +++++++++++++++++----
 .../console-fe/src/service/globalLockInfo.ts       |   3 +-
 .../console-fe/src/service/transactionInfo.ts      |  34 ++
 .../static/console-fe/src/utils/request.ts         |  53 +--
 .../src/{locales/index.d.ts => utils/requestV2.ts} |  15 +-
 .../namingserver/controller/NamingController.java  |   2 +-
 .../controller/NamingControllerV2.java             |  52 +++
 .../namingserver/entity/vo/v2/NamespaceVO.java     |  51 +++
 .../seata/namingserver/manager/NamingManager.java  | 119 +++++-
 .../seata/namingserver/NamingControllerTest.java   |   4 +-
 .../seata/namingserver/NamingControllerV2Test.java |  85 ++++
 .../seata/namingserver/NamingManagerTest.java      |  36 ++
 18 files changed, 982 insertions(+), 192 deletions(-)

diff --git a/changes/en-us/2.x.md b/changes/en-us/2.x.md
index 4f5f6d89cd..4f03dc2220 100644
--- a/changes/en-us/2.x.md
+++ b/changes/en-us/2.x.md
@@ -29,9 +29,11 @@ Add changes here for all PR submitted to the 2.x branch.
 - [[#7664](https://github.com/apache/incubator-seata/pull/7664)] support 
shentongdatabase XA mode
 - [[#7675](https://github.com/apache/incubator-seata/pull/7675)] support 
Oracle Batch Insert
 - [[#7663](https://github.com/apache/incubator-seata/pull/7663)] add Java 25 
support in CI configuration files
+- [[#7851](https://github.com/apache/incubator-seata/pull/7851)] add support 
for managing transaction groups
 - [[#7826](https://github.com/apache/incubator-seata/pull/7826)] Support 
HTTP/2 response handling for the Watch API in Server Raft mode
 
 
+
 ### bugfix:
 
 - [[#6476](https://github.com/apache/seata/issues/6476)] Fix SerialArray 
equals() method for multi-dimensional array comparison in Phase 2 rollback
diff --git a/changes/zh-cn/2.x.md b/changes/zh-cn/2.x.md
index 9b53ad0b04..fe38686de6 100644
--- a/changes/zh-cn/2.x.md
+++ b/changes/zh-cn/2.x.md
@@ -29,6 +29,7 @@
 - [[#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流水綫
+- [[#7851](https://github.com/apache/incubator-seata/pull/7851)] 控制台支持事务分组管理能力
 - [[#7826](https://github.com/apache/incubator-seata/pull/7826)] 在 Server Raft 
模式下,为 Watch API 提供对 HTTP/2 响应处理的支持
 
 
diff --git a/console/src/main/resources/static/console-fe/src/locales/en-us.ts 
b/console/src/main/resources/static/console-fe/src/locales/en-us.ts
index 449c61a328..c792ceec80 100644
--- a/console/src/main/resources/static/console-fe/src/locales/en-us.ts
+++ b/console/src/main/resources/static/console-fe/src/locales/en-us.ts
@@ -74,6 +74,31 @@ const enUs: ILocale = {
     forceDeleteBranchSessionTitle: 'force delete branch session',
     stopBranchSessionTitle: 'Stop branch session retry',
     startBranchSessionTitle: 'Start branch session retry',
+    createVGroupButtonLabel: 'Create VGroup',
+    createVGroupDialogTitle: 'Create VGroup',
+    createVGroupInputPlaceholder: 'Enter VGroup name',
+    createVGroupConfirmButton: 'Create',
+    createVGroupErrorMessage: 'Please select namespace, cluster and enter 
vgroup name',
+    createVGroupSuccessMessage: 'VGroup created successfully',
+    createVGroupFailMessage: 'Failed to create vgroup',
+    changeVGroupButtonLabel: 'Change VGroup',
+    changeVGroupDialogTitle: 'Change VGroup',
+    changeVGroupSuccessMessage: 'VGroup changed successfully',
+    changeVGroupFailMessage: 'Failed to change vgroup',
+    namespaceLabel: 'Namespace',
+    clusterLabel: 'Cluster',
+    originalNamespaceLabel: 'Original Namespace',
+    originalClusterLabel: 'Original Cluster',
+    selectVGroupLabel: 'Select VGroup',
+    targetNamespaceLabel: 'Target Namespace',
+    targetClusterLabel: 'Target Cluster',
+    vGroupNameLabel: 'VGroup Name',
+    confirmButtonLabel: 'Confirm',
+    selectOriginalNamespacePlaceholder: 'Select original namespace',
+    selectOriginalClusterPlaceholder: 'Select original cluster',
+    selectTargetNamespacePlaceholder: 'Select target namespace',
+    selectTargetClusterPlaceholder: 'Select target cluster',
+    selectVGroupPlaceholder: 'Select vgroup',
   },
   GlobalLockInfo: {
     title: 'GlobalLockInfo',
@@ -88,6 +113,24 @@ const enUs: ILocale = {
     operateTitle: 'operate',
     deleteGlobalLockTitle: 'Delete global lock',
   },
+  codeMessage: {
+    200: 'The server successfully returned the requested data.',
+    201: 'New or modified data successful.',
+    202: 'A request has entered the background queue (asynchronous task).',
+    204: 'Data deleted successfully.',
+    400: 'The request was made with an error, and the server did not create or 
modify data.',
+    401: 'The user does not have permission (token, username, password 
error).',
+    403: 'The user is authorized, but access is forbidden.',
+    404: 'The request is for a record that does not exist, and the server did 
not operate.',
+    406: 'The requested format is not available.',
+    410: 'The requested resource is permanently deleted and will not be 
obtained again.',
+    422: 'A validation error occurred when creating an object.',
+    500: 'An error occurred on the server, please check the server.',
+    502: 'Gateway error.',
+    503: 'Service unavailable, server temporarily overloaded or under 
maintenance.',
+    504: 'Gateway timeout.',
+    '-1000': 'Project name already exists, please use another name',
+  },
 };
 
 export default enUs;
diff --git 
a/console/src/main/resources/static/console-fe/src/locales/index.d.ts 
b/console/src/main/resources/static/console-fe/src/locales/index.d.ts
index c13f7ea7f0..2b43536e58 100644
--- a/console/src/main/resources/static/console-fe/src/locales/index.d.ts
+++ b/console/src/main/resources/static/console-fe/src/locales/index.d.ts
@@ -25,4 +25,5 @@ export interface ILocale {
   Overview: ILocaleMap;
   TransactionInfo: ILocaleMap;
   GlobalLockInfo: ILocaleMap;
+  codeMessage: ILocaleMap;
 }
diff --git a/console/src/main/resources/static/console-fe/src/locales/zh-cn.ts 
b/console/src/main/resources/static/console-fe/src/locales/zh-cn.ts
index 089802559a..7ef23b08e3 100644
--- a/console/src/main/resources/static/console-fe/src/locales/zh-cn.ts
+++ b/console/src/main/resources/static/console-fe/src/locales/zh-cn.ts
@@ -74,6 +74,31 @@ const zhCn: ILocale = {
     forceDeleteBranchSessionTitle: '强制删除分支事务',
     stopBranchSessionTitle: '停止分支事务重启',
     startBranchSessionTitle: '开启分支事务重试',
+    createVGroupButtonLabel: '创建事务分组',
+    createVGroupDialogTitle: '创建事务分组',
+    createVGroupInputPlaceholder: '请输入事务分组名称',
+    createVGroupConfirmButton: '创建',
+    createVGroupErrorMessage: '请选择命名空间、集群并输入事务分组名称',
+    createVGroupSuccessMessage: '事务分组创建成功',
+    createVGroupFailMessage: '创建事务分组失败',
+    changeVGroupButtonLabel: '修改事务分组',
+    changeVGroupDialogTitle: '修改事务分组',
+    changeVGroupSuccessMessage: '事务分组修改成功',
+    changeVGroupFailMessage: '修改事务分组失败',
+    namespaceLabel: '命名空间',
+    clusterLabel: '集群',
+    originalNamespaceLabel: '原命名空间',
+    originalClusterLabel: '原集群',
+    selectVGroupLabel: '选择事务分组',
+    targetNamespaceLabel: '目标命名空间',
+    targetClusterLabel: '目标集群',
+    vGroupNameLabel: '事务分组名称',
+    confirmButtonLabel: '确认',
+    selectOriginalNamespacePlaceholder: '选择原命名空间',
+    selectOriginalClusterPlaceholder: '选择原集群',
+    selectTargetNamespacePlaceholder: '选择目标命名空间',
+    selectTargetClusterPlaceholder: '选择目标集群',
+    selectVGroupPlaceholder: '选择事务分组',
   },
   GlobalLockInfo: {
     title: '全局锁信息',
@@ -88,6 +113,24 @@ const zhCn: ILocale = {
     operateTitle: '操作',
     deleteGlobalLockTitle: '删除全局锁',
   },
+  codeMessage: {
+    200: '服务器成功返回请求的数据。',
+    201: '新建或修改数据成功。',
+    202: '一个请求已经进入后台排队(异步任务)。',
+    204: '删除数据成功。',
+    400: '发出的请求有错误,服务器没有进行新建或修改数据的操作。',
+    401: '用户没有权限(令牌、用户名、密码错误)。',
+    403: '用户得到授权,但是访问是被禁止的。',
+    404: '发出的请求针对的是不存在的记录,服务器没有进行操作。',
+    406: '请求的格式不可得。',
+    410: '请求的资源被永久删除,且不会再得到的。',
+    422: '当创建一个对象时,发生一个验证错误。',
+    500: '服务器发生错误,请检查服务器。',
+    502: '网关错误。',
+    503: '服务不可用,服务器暂时过载或维护。',
+    504: '网关超时。',
+    '-1000': '项目名称已存在,请使用其他名称。',
+  },
 };
 
 export default zhCn;
diff --git 
a/console/src/main/resources/static/console-fe/src/pages/GlobalLockInfo/GlobalLockInfo.tsx
 
b/console/src/main/resources/static/console-fe/src/pages/GlobalLockInfo/GlobalLockInfo.tsx
index 1e45540023..075483aaf4 100644
--- 
a/console/src/main/resources/static/console-fe/src/pages/GlobalLockInfo/GlobalLockInfo.tsx
+++ 
b/console/src/main/resources/static/console-fe/src/pages/GlobalLockInfo/GlobalLockInfo.tsx
@@ -28,11 +28,10 @@ import {
   Message,
   Select
 } from '@alicloud/console-components';
-import Actions, { LinkButton } from '@alicloud/console-components-actions';
+import Actions from '@alicloud/console-components-actions';
 import { withRouter } from 'react-router-dom';
 import Page from '@/components/Page';
 import { GlobalProps } from '@/module';
-import styled, { css } from 'styled-components';
 import getData, {checkData, deleteData, GlobalLockParam } from 
'@/service/globalLockInfo';
 import PropTypes from 'prop-types';
 import moment from 'moment';
@@ -40,7 +39,7 @@ 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";
 
 const { RangePicker } = DatePicker;
 const FormItem = Form.Item;
@@ -48,7 +47,7 @@ const FormItem = Form.Item;
 type GlobalLockInfoState = {
   list: Array<any>;
   total: number;
-  namespaceOptions: Map<string, { clusters: string[], vgroups: string[] }>;
+  namespaceOptions: Map<string, { clusters: string[], clusterVgroups: {[key: 
string]: string[]} }>;
   clusters: Array<string>;
   vgroups: Array<string>;
   loading: boolean;
@@ -71,7 +70,7 @@ class GlobalLockInfo extends React.Component<GlobalProps, 
GlobalLockInfoState> {
       pageSize: 10,
       pageNum: 1,
     },
-    namespaceOptions: new Map<string, { clusters: string[], vgroups: string[] 
}>(),
+    namespaceOptions: new Map<string, { clusters: string[], clusterVgroups: 
{[key: string]: string[]} }>(),
     clusters: [],
     vgroups: [],
   }
@@ -91,7 +90,10 @@ class GlobalLockInfo extends React.Component<GlobalProps, 
GlobalLockInfoState> {
             pageSize: 10,
             pageNum: 1,
           },
-        }, () => this.search());
+        });
+        // always load namespaces so the select options can be populated and
+        // the passed namespace/cluster/vgroup are respected
+        this.loadNamespaces();
         return;
       }
     }
@@ -99,30 +101,44 @@ class GlobalLockInfo extends React.Component<GlobalProps, 
GlobalLockInfoState> {
   }
   loadNamespaces = async () => {
     try {
-      const namespaces = await fetchNamespace();
-      const namespaceOptions = new Map<string, { clusters: string[], vgroups: 
string[] }>();
+      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 = (namespaceData.clusterVgroups || {}) as {[key: 
string]: string[]};
+        const clusters = Object.keys(clusterVgroups);
         namespaceOptions.set(namespaceKey, {
-          clusters: namespaceData.clusters,
-          vgroups: namespaceData.vgroups,
+          clusters,
+          clusterVgroups,
         });
       });
       if (namespaceOptions.size > 0) {
-        // Set default namespace to the first option
+        // determine selected namespace/cluster based on existing param (from 
query) or fallback to first
+        const existingNamespace = this.state.globalLockParam.namespace;
+        const existingCluster = this.state.globalLockParam.cluster;
         const firstNamespace = Array.from(namespaceOptions.keys())[0];
-        const selectedNamespace = namespaceOptions.get(firstNamespace);
-        this.setState({
+        const selectedNamespaceKey = (existingNamespace && 
namespaceOptions.has(existingNamespace)) ? existingNamespace : firstNamespace;
+        const selectedNamespace = namespaceOptions.get(selectedNamespaceKey);
+        const clusters = selectedNamespace ? selectedNamespace.clusters : [];
+        const firstCluster = clusters.length > 0 ? clusters[0] : undefined;
+        const selectedCluster = (existingCluster && 
clusters.includes(existingCluster)) ? existingCluster : firstCluster;
+        const clusterVgroups = selectedNamespace ? 
selectedNamespace.clusterVgroups : {};
+        const selectedVgroups = selectedCluster ? 
clusterVgroups[selectedCluster] || [] : [];
+        // preserve vgroup from query if present and valid for the selected 
cluster, otherwise clear it
+        const existingVgroup = this.state.globalLockParam.vgroup;
+        const finalVgroup = (existingVgroup && 
selectedVgroups.includes(existingVgroup)) ? existingVgroup : '';
+        this.setState(prevState => ({
           namespaceOptions,
           globalLockParam: {
-            ...this.state.globalLockParam,
-            namespace: firstNamespace,
-            cluster: selectedNamespace ? selectedNamespace.clusters[0] : 
undefined,
+            ...prevState.globalLockParam,
+            namespace: selectedNamespaceKey,
+            cluster: selectedCluster,
+            vgroup: finalVgroup,
           },
           clusters: selectedNamespace ? selectedNamespace.clusters : [],
-          vgroups: selectedNamespace ? selectedNamespace.vgroups : [],
-        });
-        this.search();
+          vgroups: selectedVgroups,
+        }));
+       this.search();
       } else {
         this.setState({
           namespaceOptions,
@@ -139,6 +155,8 @@ class GlobalLockInfo extends React.Component<GlobalProps, 
GlobalLockInfoState> {
         pageSize: this.state.globalLockParam.pageSize,
         pageNum: this.state.globalLockParam.pageNum,
       },
+      clusters: [],
+      vgroups: [],
     });
   }
 
@@ -147,13 +165,15 @@ class GlobalLockInfo extends React.Component<GlobalProps, 
GlobalLockInfoState> {
     getData(this.state.globalLockParam).then(data => {
       // if the result set is empty, set the page number to go back to the 
first page
       if (data.total === 0) {
-        this.setState({
+        this.setState(prevState => ({
           list: [],
           total: 0,
           loading: false,
-          globalLockParam: Object.assign(this.state.globalLockParam,
-            { pageNum: 1 }),
-        });
+          globalLockParam: {
+            ...prevState.globalLockParam,
+            pageNum: 1,
+          },
+        }));
         return;
       }
       // format time
@@ -169,51 +189,90 @@ class GlobalLockInfo extends React.Component<GlobalProps, 
GlobalLockInfoState> {
         total: data.total,
         loading: false,
       });
-    }).catch(err => {
+    }).catch(() => {
       this.setState({ loading: false });
     });
   }
 
   createTimeOnChange = (value: Array<any>) => {
     // timestamp(milliseconds)
-    const timeStart = value[0] == null ? null : moment(value[0]).unix() * 1000;
-    const timeEnd = value[1] == null ? null : moment(value[1]).unix() * 1000;
-    this.setState({
-      globalLockParam: Object.assign(this.state.globalLockParam,
-        { timeStart, timeEnd }),
-    });
+    const timeStart: number | undefined = value[0] == null ? undefined : 
moment(value[0]).unix() * 1000;
+    const timeEnd: number | undefined = value[1] == null ? undefined : 
moment(value[1]).unix() * 1000;
+    this.setState(prevState => ({
+      globalLockParam: {
+        ...prevState.globalLockParam,
+        timeStart,
+        timeEnd,
+      },
+    }));
   }
 
   searchFilterOnChange = (key:string, val:string) => {
     if (key === 'namespace') {
       const selectedNamespace = this.state.namespaceOptions.get(val);
-      this.setState({
-        clusters: selectedNamespace ? selectedNamespace.clusters : [],
-        vgroups: selectedNamespace ? selectedNamespace.vgroups : [],
-        globalLockParam: Object.assign(this.state.globalLockParam, {[key]: 
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(prevState => ({
+        clusters,
+        vgroups,
+        globalLockParam: {
+          ...prevState.globalLockParam,
+          [key]: val,
+          cluster: firstCluster,
+          vgroup: '',
+        },
+      }));
+    } 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(prevState => ({
+          vgroups: selectedVgroups,
+          globalLockParam: {
+            ...prevState.globalLockParam,
+            [key]: val,
+            vgroup: '',
+          },
+        }));
+      } else {
+        this.setState(prevState => ({
+          globalLockParam: {
+            ...prevState.globalLockParam,
+            [key]: val,
+            vgroup: '',
+          },
+        }));
+      }
     } else {
-      this.setState({
-        globalLockParam: Object.assign(this.state.globalLockParam,
-            {[key]: val}),
-      });
+      this.setState(prevState => ({
+        globalLockParam: {
+          ...prevState.globalLockParam,
+          [key]: val,
+        },
+      }));
     }
   }
 
-  paginationOnChange = (current: number, e: {}) => {
-    this.setState({
-      globalLockParam: Object.assign(this.state.globalLockParam,
-        { pageNum: current }),
-    });
-    this.search();
+  paginationOnChange = (current: number, _e?: any) => {
+    this.setState(prevState => ({
+      globalLockParam: {
+        ...prevState.globalLockParam,
+        pageNum: current,
+      },
+    }), this.search);
   }
 
   paginationOnPageSizeChange = (pageSize: number) => {
-    this.setState({
-      globalLockParam: Object.assign(this.state.globalLockParam,
-        { pageSize }),
-    });
-    this.search();
+    this.setState(prevState => ({
+      globalLockParam: {
+        ...prevState.globalLockParam,
+        pageSize,
+      },
+    }), this.search);
   }
 
   deleteCell = (val: string, index: number, record: any) => {
@@ -347,6 +406,8 @@ class GlobalLockInfo extends React.Component<GlobalProps, 
GlobalLockInfoState> {
                   this.searchFilterOnChange('vgroup', value);
                 }}
                 dataSource={this.state.vgroups.map(value => ({ label: value, 
value }))}
+                value={this.state.globalLockParam.vgroup}
+                key={this.state.globalLockParam.cluster}
             />
           </FormItem>
           {/* {reset search filter button} */}
@@ -393,4 +454,3 @@ class GlobalLockInfo extends React.Component<GlobalProps, 
GlobalLockInfoState> {
 }
 
 export default withRouter(ConfigProvider.config(GlobalLockInfo, {}));
-
diff --git 
a/console/src/main/resources/static/console-fe/src/pages/TransactionInfo/TransactionInfo.tsx
 
b/console/src/main/resources/static/console-fe/src/pages/TransactionInfo/TransactionInfo.tsx
index 64a54d1623..1b329a229a 100644
--- 
a/console/src/main/resources/static/console-fe/src/pages/TransactionInfo/TransactionInfo.tsx
+++ 
b/console/src/main/resources/static/console-fe/src/pages/TransactionInfo/TransactionInfo.tsx
@@ -20,9 +20,8 @@ import Actions, { LinkButton } from 
'@alicloud/console-components-actions';
 import { withRouter } from 'react-router-dom';
 import Page from '@/components/Page';
 import { GlobalProps } from '@/module';
-import styled, { css } from 'styled-components';
 import getData, { changeGlobalData, deleteBranchData, deleteGlobalData, 
GlobalSessionParam, sendGlobalCommitOrRollback,
-  startBranchData, startGlobalData, stopBranchData, stopGlobalData, 
forceDeleteGlobalData, forceDeleteBranchData, fetchNamespace } from 
'@/service/transactionInfo';
+  startBranchData, startGlobalData, stopBranchData, stopGlobalData, 
forceDeleteGlobalData, forceDeleteBranchData, fetchNamespaceV2, addGroup, 
changeGroup } from '@/service/transactionInfo';
 import PropTypes from 'prop-types';
 import moment from 'moment';
 
@@ -48,9 +47,22 @@ type TransactionInfoState = {
   xid : string;
   currentBranchSession: Array<any>;
   globalSessionParam : GlobalSessionParam;
-  namespaceOptions: Map<string, { clusters: string[], vgroups: string[] }>;
+  namespaceOptions: Map<string, { clusters: string[], clusterVgroups: {[key: 
string]: string[]} }>;
   clusters: Array<string>;
   vgroups: Array<string>;
+  createVGroupDialogVisible: boolean;
+  vGroupName: string;
+  changeVGroupDialogVisible: boolean;
+  selectedVGroup: string;
+  targetNamespace: string;
+  targetClusters: Array<string>;
+  targetCluster: string;
+  originalNamespace: string;
+  originalClusters: Array<string>;
+  originalCluster: string;
+  originalVGroups: Array<string>;
+  createNamespace: string;
+  createCluster: string;
 }
 
 const statusList:Array<StatusType> = [
@@ -293,6 +305,8 @@ const warnning = new Map([
     ['SAGA', 'The force delete will only delete session in server.']])],
 ])
 
+const VGROUP_REFRESH_DELAY_MS = 5000;
+
 class TransactionInfo extends React.Component<GlobalProps, 
TransactionInfoState> {
   static displayName = 'TransactionInfo';
 
@@ -313,9 +327,22 @@ class TransactionInfo extends React.Component<GlobalProps, 
TransactionInfoState>
       pageSize: 10,
       pageNum: 1,
     },
-    namespaceOptions: new Map<string, { clusters: string[], vgroups: string[] 
}>(),
+    namespaceOptions: new Map<string, { clusters: string[], clusterVgroups: 
{[key: string]: string[]} }>(),
     clusters: [],
     vgroups: [],
+    createVGroupDialogVisible: false,
+    vGroupName: '',
+    changeVGroupDialogVisible: false,
+    selectedVGroup: '',
+    targetNamespace: '',
+    targetClusters: [],
+    targetCluster: '',
+    originalNamespace: '',
+    originalClusters: [],
+    originalCluster: '',
+    originalVGroups: [],
+    createNamespace: '',
+    createCluster: '',
   };
   componentDidMount = () => {
     // search once by default
@@ -323,48 +350,55 @@ class TransactionInfo extends 
React.Component<GlobalProps, TransactionInfoState>
   }
   loadNamespaces = async () => {
     try {
-      const namespaces = await fetchNamespace();
-      const namespaceOptions = new Map<string, { clusters: string[], vgroups: 
string[] }>();
+      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 = (namespaceData.clusterVgroups || {}) as {[key: 
string]: string[]};
+        const clusters = Object.keys(clusterVgroups);
         namespaceOptions.set(namespaceKey, {
-          clusters: namespaceData.clusters,
-          vgroups: namespaceData.vgroups,
+          clusters,
+          clusterVgroups,
         });
       });
         if (namespaceOptions.size > 0) {
             // Set default namespace to the first option
             const firstNamespace = Array.from(namespaceOptions.keys())[0];
             const selectedNamespace = namespaceOptions.get(firstNamespace);
-            this.setState({
+            const firstCluster = selectedNamespace ? 
selectedNamespace.clusters[0] : undefined;
+            const clusterVgroups = selectedNamespace ? 
selectedNamespace.clusterVgroups : {};
+            const selectedVgroups = firstCluster ? 
clusterVgroups[firstCluster] || [] : [];
+            this.setState(prevState => ({
                 namespaceOptions,
                 globalSessionParam: {
-                    ...this.state.globalSessionParam,
+                    ...prevState.globalSessionParam,
                     namespace: firstNamespace,
-                    cluster: selectedNamespace ? selectedNamespace.clusters[0] 
: undefined,
+                    cluster: firstCluster,
                 },
                 clusters: selectedNamespace ? selectedNamespace.clusters : [],
-                vgroups: selectedNamespace ? selectedNamespace.vgroups : [],
-            });
-            this.search();
-        } else {
-            this.setState({
-                namespaceOptions,
-            });
-        }
+                vgroups: selectedVgroups,
+            }));
+             this.search();
+         } else {
+             this.setState({
+                 namespaceOptions,
+             });
+         }
     } catch (error) {
       console.error('Failed to fetch namespaces:', error);
     }
   }
   resetSearchFilter = () => {
-    this.setState({
+    this.setState(prevState => ({
       globalSessionParam: {
         withBranch: false,
         // pagination info don`t reset
-        pageSize: this.state.globalSessionParam.pageSize,
-        pageNum: this.state.globalSessionParam.pageNum,
+        pageSize: prevState.globalSessionParam.pageSize,
+        pageNum: prevState.globalSessionParam.pageNum,
       },
-    });
+      clusters: [],
+      vgroups: [],
+    }));
   }
 
   search = () => {
@@ -372,13 +406,12 @@ class TransactionInfo extends 
React.Component<GlobalProps, TransactionInfoState>
     getData(this.state.globalSessionParam).then(data => {
       // if the result set is empty, set the page number to go back to the 
first page
       if (data.total === 0) {
-        this.setState({
+        this.setState(prevState => ({
           list: [],
           total: 0,
           loading: false,
-          globalSessionParam: Object.assign(this.state.globalSessionParam,
-            { pageNum: 1 }),
-        });
+          globalSessionParam: { ...prevState.globalSessionParam, pageNum: 1 },
+        }));
         return;
       }
       // format time
@@ -397,18 +430,21 @@ class TransactionInfo extends 
React.Component<GlobalProps, TransactionInfoState>
       });
 
       if (this.state.branchSessionDialogVisible) {
-        data.data.forEach((item: any) => {
-          if (item.xid == this.state.xid) {
-            this.state.currentBranchSession = item.branchSessionVOs
-          }
-        })
+        const currentBranchSession = data.data.find((item: any) => item.xid == 
this.state.xid)?.branchSessionVOs || [];
+        this.setState({
+          list: data.data,
+          total: data.total,
+          loading: false,
+          currentBranchSession,
+        });
+      } else {
+        this.setState({
+          list: data.data,
+          total: data.total,
+          loading: false,
+        });
       }
-      this.setState({
-        list: data.data,
-        total: data.total,
-        loading: false,
-      });
-    }).catch(err => {
+    }).catch(() => {
       this.setState({ loading: false });
     });
   }
@@ -416,23 +452,41 @@ class TransactionInfo extends 
React.Component<GlobalProps, TransactionInfoState>
   searchFilterOnChange = (key: string, val: string) => {
     if (key === 'namespace') {
       const selectedNamespace = this.state.namespaceOptions.get(val);
-      this.setState({
-        clusters: selectedNamespace ? selectedNamespace.clusters : [],
-        vgroups: selectedNamespace ? selectedNamespace.vgroups : [],
-        globalSessionParam: Object.assign(this.state.globalSessionParam, 
{[key]: 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(prevState => ({
+        clusters,
+        vgroups,
+        globalSessionParam: { ...prevState.globalSessionParam, [key]: val, 
cluster: firstCluster, vgroup: '' },
+      }));
+    } 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(prevState => ({
+          vgroups: selectedVgroups,
+          globalSessionParam: { ...prevState.globalSessionParam, [key]: val, 
vgroup: '' },
+        }));
+      } else {
+        this.setState(prevState => ({
+          globalSessionParam: { ...prevState.globalSessionParam, [key]: val, 
vgroup: '' },
+        }));
+      }
     } else {
-      this.setState({
-        globalSessionParam: Object.assign(this.state.globalSessionParam, 
{[key]: val}),
-      });
+      this.setState(prevState => ({
+        globalSessionParam: { ...prevState.globalSessionParam, [key]: val },
+      }));
     }
   };
 
-  branchSessionSwitchOnChange = (checked: boolean, e: any) => {
-    this.setState({
-      globalSessionParam: Object.assign(this.state.globalSessionParam,
-        { withBranch: checked }),
-    });
+  branchSessionSwitchOnChange = (checked: boolean, _e?: any) => {
+    this.setState(prevState => ({
+      globalSessionParam: { ...prevState.globalSessionParam, withBranch: 
checked },
+    }));
     if (checked) {
       // if checked, do search for load branch sessions
       this.search();
@@ -441,15 +495,14 @@ class TransactionInfo extends 
React.Component<GlobalProps, TransactionInfoState>
 
   createTimeOnChange = (value: Array<any>) => {
     // timestamp(milliseconds)
-    const timeStart = value[0] == null ? null : moment(value[0]).unix() * 1000;
-    const timeEnd = value[1] == null ? null : moment(value[1]).unix() * 1000;
-    this.setState({
-      globalSessionParam: Object.assign(this.state.globalSessionParam,
-        { timeStart, timeEnd }),
-    });
+    const timeStart = value[0] == null ? undefined : moment(value[0]).unix() * 
1000;
+    const timeEnd = value[1] == null ? undefined : moment(value[1]).unix() * 
1000;
+    this.setState(prevState => ({
+      globalSessionParam: { ...prevState.globalSessionParam, timeStart, 
timeEnd },
+    }));
   }
 
-  statusCell = (val: number, index: number, record: any) => {
+  statusCell = (val: number, _index?: number, _record?: any) => {
     let icon;
     statusList.forEach((status: StatusType) => {
       if (status.value === val) {
@@ -465,7 +518,7 @@ class TransactionInfo extends React.Component<GlobalProps, 
TransactionInfoState>
     return icon;
   }
 
-  branchSessionStatusCell = (val: number, index: number, record: any) => {
+  branchSessionStatusCell = (val: number, _index?: number, _record?: any) => {
     let icon;
     branchSessionStatusList.forEach((status: StatusType) => {
       if (status.value === val) {
@@ -525,6 +578,7 @@ class TransactionInfo extends React.Component<GlobalProps, 
TransactionInfoState>
           {showGlobalLockTitle}
         </LinkButton>
 
+
         <Button
           onClick={() => {
             Dialog.confirm({
@@ -545,7 +599,7 @@ class TransactionInfo extends React.Component<GlobalProps, 
TransactionInfoState>
                   title: 'Warnning',
                   content: <div dangerouslySetInnerHTML={{__html: 
commonWarnning + '<br>' + warnMessage}}/>,
                   onOk: () => {
-                    deleteGlobalData(record).then((rsp) => {
+                    deleteGlobalData(record).then(() => {
                       Message.success("Delete successfully")
                       this.search()
                     }).catch((rsp) => {
@@ -580,7 +634,7 @@ class TransactionInfo extends React.Component<GlobalProps, 
TransactionInfoState>
                   title: 'Warnning',
                   content: <div dangerouslySetInnerHTML={{__html: 
commonWarnning + '<br>' + warnMessage}}/>,
                   onOk: () => {
-                    forceDeleteGlobalData(record).then((rsp) => {
+                    forceDeleteGlobalData(record).then(() => {
                       Message.success("Delete successfully")
                       this.search()
                     }).catch((rsp) => {
@@ -602,7 +656,7 @@ class TransactionInfo extends React.Component<GlobalProps, 
TransactionInfoState>
                 title: 'Confirm',
                 content: 'Are you sure you want to start restart global 
transactions',
                 onOk: () => {
-                  startGlobalData(record).then((rsp) => {
+                  startGlobalData(record).then(() => {
                     Message.success("Start successfully")
                     this.search()
                   }).catch((rsp) => {
@@ -619,7 +673,7 @@ class TransactionInfo extends React.Component<GlobalProps, 
TransactionInfoState>
             title: 'Confirm',
             content: 'Are you sure you want to stop stop global transactions',
             onOk: () => {
-              stopGlobalData(record).then((rsp) => {
+              stopGlobalData(record).then(() => {
                 Message.success("Stop successfully")
                 this.search()
               }).catch((rsp) => {
@@ -639,7 +693,7 @@ class TransactionInfo extends React.Component<GlobalProps, 
TransactionInfoState>
               title: 'Confirm',
               content: 'Are you sure you want to send commit or rollback to 
global transactions',
               onOk: () => {
-                sendGlobalCommitOrRollback(record).then((rsp) => {
+                sendGlobalCommitOrRollback(record).then(() => {
                   Message.success("Send successfully")
                   this.search()
                 }).catch((rsp) => {
@@ -658,7 +712,7 @@ class TransactionInfo extends React.Component<GlobalProps, 
TransactionInfoState>
               title: 'Confirm',
               content: 'Are you sure you want to change the global 
transactions status',
               onOk: () => {
-                changeGlobalData(record).then((rsp) => {
+                changeGlobalData(record).then(() => {
                   Message.success("Change successfully")
                   this.search()
                 }).catch((rsp) => {
@@ -718,7 +772,7 @@ class TransactionInfo extends React.Component<GlobalProps, 
TransactionInfoState>
                   title: 'Warnning',
                   content: <div dangerouslySetInnerHTML={{__html: 
commonWarnning + '<br>' + warnMessage}}/>,
                   onOk: () => {
-                    deleteBranchData(record).then((rsp) => {
+                    deleteBranchData(record).then(() => {
                       Message.success("Delete successfully")
                       this.search()
                     }).catch((rsp) => {
@@ -744,7 +798,7 @@ class TransactionInfo extends React.Component<GlobalProps, 
TransactionInfoState>
                   title: 'Warnning',
                   content: <div dangerouslySetInnerHTML={{__html: 
commonWarnning + '<br>' + warnMessage}}/>,
                   onOk: () => {
-                    forceDeleteBranchData(record).then((rsp) => {
+                    forceDeleteBranchData(record).then(() => {
                       Message.success("Delete successfully")
                       this.search()
                     }).catch((rsp) => {
@@ -766,7 +820,7 @@ class TransactionInfo extends React.Component<GlobalProps, 
TransactionInfoState>
                 title: 'Confirm',
                 content: 'Are you sure you want to start branch transactions 
retry',
                 onOk: () => {
-                  startBranchData(record).then((rsp) => {
+                  startBranchData(record).then(() => {
                     Message.success("Start successfully")
                     this.search()
                   }).catch((rsp) => {
@@ -789,7 +843,7 @@ class TransactionInfo extends React.Component<GlobalProps, 
TransactionInfoState>
                   title: 'Warnning',
                   content: <div dangerouslySetInnerHTML={{__html: 
commonWarnning + '<br>' + warnMessage}}/>,
                   onOk: () => {
-                    stopBranchData(record).then((rsp) => {
+                    stopBranchData(record).then(() => {
                       Message.success("Stop successfully")
                       this.search()
                     }).catch((rsp) => {
@@ -807,19 +861,17 @@ class TransactionInfo extends 
React.Component<GlobalProps, TransactionInfoState>
       </Actions>);
   }
 
-  paginationOnChange = (current: number, e: {}) => {
-    this.setState({
-      globalSessionParam: Object.assign(this.state.globalSessionParam,
-        { pageNum: current }),
-    });
+  paginationOnChange = (current: number, _e?: any) => {
+    this.setState(prevState => ({
+      globalSessionParam: { ...prevState.globalSessionParam, pageNum: current 
},
+    }));
     this.search();
   }
 
   paginationOnPageSizeChange = (pageSize: number) => {
-    this.setState({
-      globalSessionParam: Object.assign(this.state.globalSessionParam,
-        { pageSize }),
-    });
+    this.setState(prevState => ({
+      globalSessionParam: { ...prevState.globalSessionParam, pageSize },
+    }));
     this.search();
   }
 
@@ -839,6 +891,95 @@ class TransactionInfo extends React.Component<GlobalProps, 
TransactionInfoState>
     });
   }
 
+  showCreateVGroupDialog = () => {
+    this.setState(prevState => ({
+      createVGroupDialogVisible: true,
+      vGroupName: '',
+      createNamespace: prevState.globalSessionParam.namespace || '',
+      createCluster: prevState.globalSessionParam.cluster || '',
+    }));
+  }
+
+  closeCreateVGroupDialog = () => {
+    this.setState({
+      createVGroupDialogVisible: false,
+      vGroupName: '',
+      createNamespace: '',
+      createCluster: '',
+    });
+  }
+
+  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 { createNamespace, createCluster, vGroupName } = this.state;
+    if (!createNamespace || !createCluster || !vGroupName.trim()) {
+      Message.error(createVGroupErrorMessage);
+      return;
+    }
+    addGroup(createNamespace, createCluster, 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(() => 
{
+      Message.success(changeVGroupSuccessMessage);
+      this.closeChangeVGroupDialog();
+      // 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 ? `${changeVGroupFailMessage}: 
${backendMessage}` : changeVGroupFailMessage;
+      Message.error(displayMessage);
+    });
+  }
+
   render() {
     const { locale = {} } = this.props;
     const { title, subTitle, createTimeLabel,
@@ -852,6 +993,26 @@ class TransactionInfo extends React.Component<GlobalProps, 
TransactionInfoState>
       searchButtonLabel,
       operateTitle,
       branchSessionDialogTitle,
+      createVGroupButtonLabel,
+      createVGroupDialogTitle,
+      createVGroupInputPlaceholder,
+      createVGroupConfirmButton,
+      changeVGroupButtonLabel,
+      changeVGroupDialogTitle,
+      namespaceLabel,
+      clusterLabel,
+      originalNamespaceLabel,
+      originalClusterLabel,
+      selectVGroupLabel,
+      targetNamespaceLabel,
+      targetClusterLabel,
+      vGroupNameLabel,
+      confirmButtonLabel,
+      selectOriginalNamespacePlaceholder,
+      selectOriginalClusterPlaceholder,
+      selectTargetNamespacePlaceholder,
+      selectTargetClusterPlaceholder,
+      selectVGroupPlaceholder,
     } = locale;
     return (
       <Page
@@ -928,6 +1089,8 @@ class TransactionInfo extends React.Component<GlobalProps, 
TransactionInfoState>
                   this.searchFilterOnChange('vgroup', value);
                 }}
                 dataSource={this.state.vgroups.map(value => ({ label: value, 
value }))}
+                value={this.state.globalSessionParam.vgroup}
+                key={this.state.globalSessionParam.cluster}
             />
           </FormItem>
           {/* {branch session switch} */}
@@ -949,6 +1112,18 @@ class TransactionInfo extends 
React.Component<GlobalProps, TransactionInfoState>
               <Icon type="search" />{searchButtonLabel}
             </Form.Submit>
           </FormItem>
+          {/* {create vgroup button} */}
+          <FormItem>
+            <Button onClick={this.showCreateVGroupDialog}>
+              {createVGroupButtonLabel}
+            </Button>
+          </FormItem>
+          {/* {change vgroup button} */}
+          <FormItem>
+            <Button onClick={this.showChangeVGroupDialog}>
+              {changeVGroupButtonLabel}
+            </Button>
+          </FormItem>
         </Form>
         {/* global session table */}
         <div>
@@ -1004,6 +1179,143 @@ class TransactionInfo extends 
React.Component<GlobalProps, TransactionInfoState>
             />
           </Table>
         </Dialog>
+
+        {/* create vgroup dialog */}
+        <Dialog visible={this.state.createVGroupDialogVisible} 
title={createVGroupDialogTitle} footer={false} 
onClose={this.closeCreateVGroupDialog}>
+          <Form inline labelAlign="left">
+            <FormItem name="createNamespace" label={namespaceLabel}>
+              <Select
+                placeholder={selectNamespaceFilerPlaceholder}
+                onChange={(value: string) => {
+                  this.setState(prevState => {
+                    const clusters = value ? 
prevState.namespaceOptions.get(value)?.clusters || [] : [];
+                    return {
+                      createNamespace: value,
+                      createCluster: clusters.length > 0 ? clusters[0] : '',
+                    };
+                  });
+                }}
+                
dataSource={Array.from(this.state.namespaceOptions.keys()).map(key => ({ label: 
key, value: key }))}
+                value={this.state.createNamespace}
+              />
+            </FormItem>
+            <FormItem name="createCluster" label={clusterLabel}>
+              <Select
+                placeholder={selectClusterFilerPlaceholder}
+                onChange={(value: string) => {
+                  this.setState({ createCluster: value });
+                }}
+                
dataSource={this.state.namespaceOptions.get(this.state.createNamespace)?.clusters.map(value
 => ({ label: value, value })) || []}
+                value={this.state.createCluster}
+              />
+            </FormItem>
+            <FormItem name="vGroupName" label={vGroupNameLabel}>
+              <Input
+                placeholder={createVGroupInputPlaceholder}
+                value={this.state.vGroupName}
+                onChange={(value: string) => { this.setState({ vGroupName: 
value }); }}
+              />
+            </FormItem>
+            <FormItem>
+              <Button type="primary" onClick={this.handleCreateVGroup}>
+                {createVGroupConfirmButton}
+              </Button>
+            </FormItem>
+          </Form>
+        </Dialog>
+
+        {/* change vgroup dialog */}
+        <Dialog visible={this.state.changeVGroupDialogVisible} 
title={changeVGroupDialogTitle} footer={false} 
onClose={this.closeChangeVGroupDialog}>
+          <Form inline labelAlign="left">
+            <FormItem name="originalNamespace" label={originalNamespaceLabel}>
+              <Select
+                hasClear
+                placeholder={selectOriginalNamespacePlaceholder}
+                onChange={(value: string) => {
+                  this.setState(prevState => {
+                    const clusters = value ? 
prevState.namespaceOptions.get(value)?.clusters || [] : [];
+                    const firstCluster = clusters.length > 0 ? clusters[0] : 
'';
+                    const namespaceData = 
prevState.namespaceOptions.get(value);
+                    const clusterVgroups = namespaceData ? 
namespaceData.clusterVgroups : {};
+                    const vgroups = firstCluster ? 
clusterVgroups[firstCluster] || [] : [];
+                    return {
+                      originalNamespace: value,
+                      originalClusters: clusters,
+                      originalCluster: firstCluster,
+                      originalVGroups: vgroups,
+                    };
+                  });
+                }}
+                
dataSource={Array.from(this.state.namespaceOptions.keys()).map(key => ({ label: 
key, value: key }))}
+                value={this.state.originalNamespace}
+              />
+            </FormItem>
+            <FormItem name="originalCluster" label={originalClusterLabel}>
+              <Select
+                hasClear
+                placeholder={selectOriginalClusterPlaceholder}
+                onChange={(value: string) => {
+                  this.setState(prevState => {
+                    const namespaceData = 
prevState.namespaceOptions.get(prevState.originalNamespace);
+                    const clusterVgroups = namespaceData ? 
namespaceData.clusterVgroups : {};
+                    const vgroups = clusterVgroups[value] || [];
+                    return {
+                      originalCluster: value,
+                      originalVGroups: vgroups,
+                    };
+                  });
+                }}
+                dataSource={this.state.originalClusters.map(value => ({ label: 
value, value }))}
+                value={this.state.originalCluster}
+              />
+            </FormItem>
+            <FormItem name="selectedVGroup" label={selectVGroupLabel}>
+              <Select
+                hasClear
+                placeholder={selectVGroupPlaceholder}
+                onChange={(value: string) => {
+                  this.setState({ selectedVGroup: value });
+                }}
+                dataSource={this.state.originalVGroups.map(value => ({ label: 
value, value }))}
+                value={this.state.selectedVGroup}
+              />
+            </FormItem>
+            <FormItem name="targetNamespace" label={targetNamespaceLabel}>
+              <Select
+                hasClear
+                placeholder={selectTargetNamespacePlaceholder}
+                onChange={(value: string) => {
+                  this.setState(prevState => {
+                    const clusters = value ? 
prevState.namespaceOptions.get(value)?.clusters || [] : [];
+                    return {
+                      targetNamespace: value,
+                      targetClusters: clusters,
+                      targetCluster: clusters.length > 0 ? clusters[0] : '',
+                    };
+                  });
+                }}
+                
dataSource={Array.from(this.state.namespaceOptions.keys()).map(key => ({ label: 
key, value: key }))}
+                value={this.state.targetNamespace}
+              />
+            </FormItem>
+            <FormItem name="targetCluster" label={targetClusterLabel}>
+              <Select
+                hasClear
+                placeholder={selectTargetClusterPlaceholder}
+                onChange={(value: string) => {
+                  this.setState({ targetCluster: value });
+                }}
+                dataSource={this.state.targetClusters.map(value => ({ label: 
value, value }))}
+                value={this.state.targetCluster}
+              />
+            </FormItem>
+            <FormItem>
+              <Button type="primary" onClick={this.handleChangeVGroup} 
disabled={!this.state.selectedVGroup || !this.state.targetNamespace || 
!this.state.targetCluster}>
+                {confirmButtonLabel}
+              </Button>
+            </FormItem>
+          </Form>
+        </Dialog>
       </Page>
     );
   }
diff --git 
a/console/src/main/resources/static/console-fe/src/service/globalLockInfo.ts 
b/console/src/main/resources/static/console-fe/src/service/globalLockInfo.ts
index fa59b8e776..e22714f736 100644
--- a/console/src/main/resources/static/console-fe/src/service/globalLockInfo.ts
+++ b/console/src/main/resources/static/console-fe/src/service/globalLockInfo.ts
@@ -15,6 +15,7 @@
  * limitations under the License.
  */
 import request from '@/utils/request';
+import requestV2 from '@/utils/requestV2';
 
 export type GlobalLockParam = {
   xid?: string,
@@ -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', {
     method: 'get',
   });
   return result.data;
diff --git 
a/console/src/main/resources/static/console-fe/src/service/transactionInfo.ts 
b/console/src/main/resources/static/console-fe/src/service/transactionInfo.ts
index eb81045537..da99ed5958 100644
--- 
a/console/src/main/resources/static/console-fe/src/service/transactionInfo.ts
+++ 
b/console/src/main/resources/static/console-fe/src/service/transactionInfo.ts
@@ -15,6 +15,7 @@
  * limitations under the License.
  */
 import request from '@/utils/request';
+import requestV2 from '@/utils/requestV2';
 
 export type GlobalSessionParam = {
   xid?: string,
@@ -49,6 +50,13 @@ export async function fetchNamespace():Promise<any> {
   return result.data;
 }
 
+export async function fetchNamespaceV2():Promise<any> {
+  const result = await requestV2.get('/naming/namespace', {
+    method: 'get',
+  });
+  return result.data;
+}
+
 export default async function 
fetchData(params:GlobalSessionParam):Promise<any> {
   let result = await request('/console/globalSession/query', {
     method: 'get',
@@ -239,3 +247,29 @@ export async function startBranchData(params: 
BranchSessionParam): Promise<any>
   });
   return result;
 }
+
+export async function addGroup(namespace: string, clusterName: string, vGroup: 
string, unitName?: string): Promise<any> {
+  let result = await request('/naming/addGroup', {
+    method: 'POST',
+    params: {
+      namespace,
+      clusterName,
+      vGroup,
+      unitName,
+    },
+  });
+  return result;
+}
+
+export async function changeGroup(namespace: string, clusterName: string, 
vGroup: string, unitName?: string): Promise<any> {
+  let result = await request('/naming/changeGroup', {
+    method: 'POST',
+    params: {
+      namespace,
+      clusterName,
+      vGroup,
+      unitName,
+    },
+  });
+  return result;
+}
diff --git a/console/src/main/resources/static/console-fe/src/utils/request.ts 
b/console/src/main/resources/static/console-fe/src/utils/request.ts
index 47ab759761..d41e2119d5 100644
--- a/console/src/main/resources/static/console-fe/src/utils/request.ts
+++ b/console/src/main/resources/static/console-fe/src/utils/request.ts
@@ -14,47 +14,26 @@
  * 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> => {
@@ -62,12 +41,13 @@ const request = () => {
       if (response.status === 200 && code === '200') {
         return Promise.resolve(get(response, 'data'));
       } else {
+        const currentLocale = getCurrentLocaleObj();
         const errorText =
-          (codeMessage as any)[code] ||
+          (currentLocale.codeMessage as any)[code] ||
           get(response, 'data.message') ||
           get(response, 'data.errorMsg') ||
           response.statusText;
-        Message.error(errorText || `请求错误 ${code}: ${get(response, 
'config.url', '')}`);
+        Message.error(errorText || `Request error ${code}: ${get(response, 
'config.url', '')}`);
         return Promise.reject(response);
       }
     },
@@ -80,7 +60,7 @@ const request = () => {
         }
         Message.error(`HTTP ERROR: ${status}`);
       } else {
-        Message.error(API_GENERAL_ERROR_MESSAGE);
+        Message.error(generalErrorMessage);
       }
       return Promise.reject(error);
     }
@@ -89,4 +69,7 @@ const request = () => {
   return instance;
 };
 
-export default request();
+const request = createRequest('api/v1');
+
+export { createRequest };
+export default request;
diff --git 
a/console/src/main/resources/static/console-fe/src/locales/index.d.ts 
b/console/src/main/resources/static/console-fe/src/utils/requestV2.ts
similarity index 76%
copy from console/src/main/resources/static/console-fe/src/locales/index.d.ts
copy to console/src/main/resources/static/console-fe/src/utils/requestV2.ts
index c13f7ea7f0..f467fc0c6b 100644
--- a/console/src/main/resources/static/console-fe/src/locales/index.d.ts
+++ b/console/src/main/resources/static/console-fe/src/utils/requestV2.ts
@@ -14,15 +14,8 @@
  * See the License for the specific language governing permissions and
  * limitations under the License.
  */
+import { createRequest } from './request';
 
-export interface ILocaleMap {
-    [key: string]: string
-}
-export interface ILocale {
-  MenuRouter: ILocaleMap;
-  Header: ILocaleMap;
-  Login: ILocaleMap;
-  Overview: ILocaleMap;
-  TransactionInfo: ILocaleMap;
-  GlobalLockInfo: ILocaleMap;
-}
+const requestV2 = createRequest('api/v2');
+
+export default requestV2;
diff --git 
a/namingserver/src/main/java/org/apache/seata/namingserver/controller/NamingController.java
 
b/namingserver/src/main/java/org/apache/seata/namingserver/controller/NamingController.java
index d70c818108..a9e423cf23 100644
--- 
a/namingserver/src/main/java/org/apache/seata/namingserver/controller/NamingController.java
+++ 
b/namingserver/src/main/java/org/apache/seata/namingserver/controller/NamingController.java
@@ -133,7 +133,7 @@ public class NamingController {
     public Result<String> changeGroup(
             @RequestParam String namespace,
             @RequestParam String clusterName,
-            @RequestParam String unitName,
+            @RequestParam(defaultValue = "", required = false) String unitName,
             @RequestParam String vGroup) {
         Result<String> addGroupResult = namingManager.changeGroup(namespace, 
vGroup, clusterName, unitName);
         if (!addGroupResult.isSuccess()) {
diff --git 
a/namingserver/src/main/java/org/apache/seata/namingserver/controller/NamingControllerV2.java
 
b/namingserver/src/main/java/org/apache/seata/namingserver/controller/NamingControllerV2.java
new file mode 100644
index 0000000000..c5396ca9ca
--- /dev/null
+++ 
b/namingserver/src/main/java/org/apache/seata/namingserver/controller/NamingControllerV2.java
@@ -0,0 +1,52 @@
+/*
+ * 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;
+
+    /**
+     * 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
+     */
+    @GetMapping("/namespace")
+    public SingleResult<Map<String, NamespaceVO>> namespaces() {
+        return namingManager.namespaceV2();
+    }
+}
diff --git 
a/namingserver/src/main/java/org/apache/seata/namingserver/entity/vo/v2/NamespaceVO.java
 
b/namingserver/src/main/java/org/apache/seata/namingserver/entity/vo/v2/NamespaceVO.java
new file mode 100644
index 0000000000..24019eb134
--- /dev/null
+++ 
b/namingserver/src/main/java/org/apache/seata/namingserver/entity/vo/v2/NamespaceVO.java
@@ -0,0 +1,51 @@
+/*
+ * 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;
+
+/**
+ * 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.
+ */
+public class NamespaceVO {
+
+    private Map<String, List<String>> clusterVgroups = new HashMap<>();
+
+    public Map<String, List<String>> getClusterVgroups() {
+        return clusterVgroups;
+    }
+
+    public void setClusterVgroups(Map<String, List<String>> clusterVgroups) {
+        this.clusterVgroups = clusterVgroups;
+    }
+}
diff --git 
a/namingserver/src/main/java/org/apache/seata/namingserver/manager/NamingManager.java
 
b/namingserver/src/main/java/org/apache/seata/namingserver/manager/NamingManager.java
index 778a36be83..9a159c8997 100644
--- 
a/namingserver/src/main/java/org/apache/seata/namingserver/manager/NamingManager.java
+++ 
b/namingserver/src/main/java/org/apache/seata/namingserver/manager/NamingManager.java
@@ -79,6 +79,22 @@ public class NamingManager {
     private final ConcurrentMap<String /* namespace */, ConcurrentMap<String 
/* clusterName */, ClusterData>>
             namespaceClusterDataMap;
 
+    // Helper class to hold collected namespace data
+    private static class NamespaceData {
+        final Map<String, Set<String>> clustersMap;
+        final Map<String, Set<String>> vgroupsMap;
+        final Map<String, Map<String, Set<String>>> clusterVgroupsMap;
+
+        NamespaceData(
+                Map<String, Set<String>> clustersMap,
+                Map<String, Set<String>> vgroupsMap,
+                Map<String, Map<String, Set<String>>> clusterVgroupsMap) {
+            this.clustersMap = clustersMap;
+            this.vgroupsMap = vgroupsMap;
+            this.clusterVgroupsMap = clusterVgroupsMap;
+        }
+    }
+
     @Value("${heartbeat.threshold:90000}")
     private int heartbeatTimeThreshold;
 
@@ -153,6 +169,16 @@ public class NamingManager {
     }
 
     public Result<String> createGroup(String namespace, String vGroup, String 
clusterName, String unitName) {
+        return createGroup(namespace, vGroup, clusterName, unitName, true);
+    }
+
+    public Result<String> createGroup(
+            String namespace, String vGroup, String clusterName, String 
unitName, boolean checkExist) {
+        // Check if vGroup already exists
+        if (checkExist && vGroupMap.getIfPresent(vGroup) != null) {
+            LOGGER.error("vGroup {} already exists", vGroup);
+            return new Result<>("400", "vGroup " + vGroup + " already exists");
+        }
         // add vGroup in new cluster
         List<Node> nodeList = getInstances(namespace, clusterName);
         if (nodeList == null || nodeList.size() == 0) {
@@ -448,7 +474,7 @@ public class NamingManager {
                     new HashSet<>(
                             
namespaceMap.get(currentNamespace).getClusterMap().keySet()));
         }
-        Result<String> res = createGroup(namespace, vGroup, clusterName, 
unitName);
+        Result<String> res = createGroup(namespace, vGroup, clusterName, 
unitName, false);
         if (!res.isSuccess()) {
             LOGGER.error("add vgroup failed!" + res.getMessage());
             return res;
@@ -477,24 +503,89 @@ public class NamingManager {
     }
 
     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);
+            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);
+    }
+
+    // Helper method to collect namespace data
+    private NamespaceData collectNamespaceData() {
+        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);
+            });
+        }));
+
+        return new NamespaceData(clustersMap, vgroupsMap, clusterVgroupsMap);
+    }
 }
diff --git 
a/namingserver/src/test/java/org/apache/seata/namingserver/NamingControllerTest.java
 
b/namingserver/src/test/java/org/apache/seata/namingserver/NamingControllerTest.java
index f418e45888..20128d1da7 100644
--- 
a/namingserver/src/test/java/org/apache/seata/namingserver/NamingControllerTest.java
+++ 
b/namingserver/src/test/java/org/apache/seata/namingserver/NamingControllerTest.java
@@ -40,7 +40,9 @@ import java.util.UUID;
 import java.util.concurrent.TimeUnit;
 
 import static org.apache.seata.common.NamingServerConstants.CONSTANT_GROUP;
-import static org.junit.jupiter.api.Assertions.*;
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertNotNull;
+import static org.junit.jupiter.api.Assertions.fail;
 
 @RunWith(SpringRunner.class)
 @SpringBootTest
diff --git 
a/namingserver/src/test/java/org/apache/seata/namingserver/NamingControllerV2Test.java
 
b/namingserver/src/test/java/org/apache/seata/namingserver/NamingControllerV2Test.java
new file mode 100644
index 0000000000..a3ebf87832
--- /dev/null
+++ 
b/namingserver/src/test/java/org/apache/seata/namingserver/NamingControllerV2Test.java
@@ -0,0 +1,85 @@
+/*
+ * 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.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertNotNull;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+@RunWith(SpringRunner.class)
+@SpringBootTest
+class NamingControllerV2Test {
+
+    @Autowired
+    NamingControllerV2 namingControllerV2;
+
+    @Autowired
+    NamingManager namingManager;
+
+    @Test
+    void testNamespaces() {
+        String clusterName = "test-cluster";
+        String namespace = "test-namespace";
+        String vGroup = "test-vGroup";
+        String unitName = String.valueOf(UUID.randomUUID());
+
+        // Register an instance to set up data
+        NamingServerNode node = new NamingServerNode();
+        node.setTransaction(new Node.Endpoint("127.0.0.1", 8091, "netty"));
+        node.setControl(new Node.Endpoint("127.0.0.1", 7091, "http"));
+        Map<String, Object> metadata = node.getMetadata();
+        Map<String, Object> vGroups = new java.util.HashMap<>();
+        vGroups.put(vGroup, unitName);
+        metadata.put(CONSTANT_GROUP, vGroups);
+        namingManager.registerInstance(node, namespace, clusterName, unitName);
+        namingManager.addGroup(namespace, clusterName, unitName, vGroup);
+
+        // Call the namespaces method
+        SingleResult<Map<String, NamespaceVO>> result = 
namingControllerV2.namespaces();
+
+        assertNotNull(result);
+        assertTrue(result.isSuccess());
+        assertEquals("200", result.getCode());
+        assertNotNull(result.getData());
+        assertTrue(result.getData().containsKey(namespace));
+
+        NamespaceVO namespaceVO = result.getData().get(namespace);
+        assertNotNull(namespaceVO);
+        assertNotNull(namespaceVO.getClusterVgroups());
+        assertTrue(namespaceVO.getClusterVgroups().containsKey(clusterName));
+        
assertTrue(namespaceVO.getClusterVgroups().get(clusterName).contains(vGroup));
+
+        // Clean up
+        namingManager.unregisterInstance(namespace, clusterName, unitName, 
node);
+    }
+}
diff --git 
a/namingserver/src/test/java/org/apache/seata/namingserver/NamingManagerTest.java
 
b/namingserver/src/test/java/org/apache/seata/namingserver/NamingManagerTest.java
index 4ac96eacd9..80e26d4b3b 100644
--- 
a/namingserver/src/test/java/org/apache/seata/namingserver/NamingManagerTest.java
+++ 
b/namingserver/src/test/java/org/apache/seata/namingserver/NamingManagerTest.java
@@ -24,8 +24,10 @@ import org.apache.seata.common.metadata.Node;
 import org.apache.seata.common.metadata.namingserver.NamingServerNode;
 import org.apache.seata.common.metadata.namingserver.Unit;
 import org.apache.seata.common.result.Result;
+import org.apache.seata.common.result.SingleResult;
 import org.apache.seata.common.util.HttpClientUtil;
 import org.apache.seata.namingserver.entity.vo.monitor.ClusterVO;
+import org.apache.seata.namingserver.entity.vo.v2.NamespaceVO;
 import org.apache.seata.namingserver.listener.ClusterChangeEvent;
 import org.apache.seata.namingserver.manager.NamingManager;
 import org.junit.jupiter.api.AfterEach;
@@ -230,6 +232,9 @@ class NamingManagerTest {
 
         Mockito.when(statusLine.getStatusCode()).thenReturn(200);
         Result<String> result = namingManager.createGroup(namespace, vGroup, 
clusterName, unitName);
+        assertFalse(result.isSuccess());
+        vGroup = "test-vGroup2";
+        result = namingManager.createGroup(namespace, vGroup, clusterName, 
unitName);
         assertTrue(result.isSuccess());
         assertEquals("200", result.getCode());
         assertEquals("add vGroup successfully!", result.getMessage());
@@ -353,4 +358,35 @@ class NamingManagerTest {
 
         Mockito.verify(applicationContext, 
Mockito.times(2)).publishEvent(any(ClusterChangeEvent.class));
     }
+
+    @Test
+    void testNamespaceV2() {
+        String namespace = "test-namespace";
+        String clusterName = "test-cluster";
+        String unitName = UUID.randomUUID().toString();
+        String vGroup = "test-vGroup";
+
+        // Register an instance
+        NamingServerNode node = createTestNode("127.0.0.1", 8080, unitName);
+        Map<String, String> vGroups = new HashMap<>();
+        vGroups.put(vGroup, unitName);
+        node.getMetadata().put(CONSTANT_GROUP, vGroups);
+        namingManager.registerInstance(node, namespace, clusterName, unitName);
+
+        // Call namespaceV2
+        SingleResult<Map<String, NamespaceVO>> result = 
namingManager.namespaceV2();
+
+        assertNotNull(result);
+        assertTrue(result.isSuccess());
+        assertEquals("200", result.getCode());
+        assertNotNull(result.getData());
+        assertTrue(result.getData().containsKey(namespace));
+
+        org.apache.seata.namingserver.entity.vo.v2.NamespaceVO namespaceVO =
+                result.getData().get(namespace);
+        assertNotNull(namespaceVO);
+        assertNotNull(namespaceVO.getClusterVgroups());
+        assertTrue(namespaceVO.getClusterVgroups().containsKey(clusterName));
+        
assertTrue(namespaceVO.getClusterVgroups().get(clusterName).contains(vGroup));
+    }
 }


---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]

Reply via email to