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]