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 5219ca3fa2 feature: console supports creation and modification of
transaction groups for Raft clusters (#7878)
5219ca3fa2 is described below
commit 5219ca3fa28297a2d66accb4253e9cbb48468ed1
Author: funkye <[email protected]>
AuthorDate: Mon Dec 22 11:24:56 2025 +0800
feature: console supports creation and modification of transaction groups
for Raft clusters (#7878)
---
changes/en-us/2.x.md | 1 +
changes/zh-cn/2.x.md | 1 +
.../main/resources/static/console-fe/src/app.tsx | 2 +-
.../static/console-fe/src/locales/en-us.ts | 1 +
.../static/console-fe/src/locales/zh-cn.ts | 1 +
.../resources/static/console-fe/src/module.d.ts | 2 +-
.../src/pages/ClusterManager/ClusterManager.tsx | 28 +++-
.../src/pages/GlobalLockInfo/GlobalLockInfo.tsx | 32 ++--
.../src/pages/TransactionInfo/TransactionInfo.tsx | 182 ++++++++++++++++-----
.../static/console-fe/src/reducers/locale.ts | 2 +-
.../console-fe/src/service/clusterManager.ts | 18 ++
.../console-fe/src/service/transactionInfo.ts | 27 +--
.../namingserver/entity/bo/NamespaceData.java | 53 ++++++
.../seata/namingserver/entity/vo/v2/ClusterVO.java | 59 +++++++
.../namingserver/entity/vo/v2/NamespaceVO.java | 15 +-
.../seata/namingserver/manager/NamingManager.java | 101 ++++++++----
namingserver/src/main/resources/application.yml | 1 -
.../seata/namingserver/NamingControllerV2Test.java | 12 +-
.../seata/namingserver/NamingManagerTest.java | 12 +-
.../server/controller/VGroupMappingController.java | 15 +-
.../controller/VGroupMappingControllerTest.java | 118 ++++++++++++-
21 files changed, 538 insertions(+), 145 deletions(-)
diff --git a/changes/en-us/2.x.md b/changes/en-us/2.x.md
index feb62120c4..8c8f7184a1 100644
--- a/changes/en-us/2.x.md
+++ b/changes/en-us/2.x.md
@@ -34,6 +34,7 @@ Add changes here for all PR submitted to the 2.x branch.
- [[#7826](https://github.com/apache/incubator-seata/pull/7826)] Support
HTTP/2 response handling for the Watch API in Server Raft mode
- [[#7870](https://github.com/apache/incubator-seata/pull/7870)] upgrade the
namingserver and console modules to JDK 25 and SpringBoot 3.5, and add the
spring-ai dependency in the console module.
- [[#7872](https://github.com/apache/incubator-seata/pull/7872)] Automatically
calculate the values for JVM parameters
+- [[#7878](https://github.com/apache/incubator-seata/pull/7878)] console
supports creation and modification of transaction groups for Raft clusters
### bugfix:
diff --git a/changes/zh-cn/2.x.md b/changes/zh-cn/2.x.md
index f1da97fd36..37e62b42fa 100644
--- a/changes/zh-cn/2.x.md
+++ b/changes/zh-cn/2.x.md
@@ -34,6 +34,7 @@
- [[#7826](https://github.com/apache/incubator-seata/pull/7826)] 在 Server Raft
模式下,为 Watch API 提供对 HTTP/2 响应处理的支持
- [[#7870](https://github.com/apache/incubator-seata/pull/7870)]
将namingserver和console模块升级到JDK 25和SpringBoot 3.5, 并在console模块中添加spring-ai依赖。
- [[#7872](https://github.com/apache/incubator-seata/pull/7872)] 根据当前内存值自动计算
JVM 参数
+- [[#7878](https://github.com/apache/incubator-seata/pull/7878)]
控制台支持raft集群模式的事务分组管理
### bugfix:
diff --git a/console/src/main/resources/static/console-fe/src/app.tsx
b/console/src/main/resources/static/console-fe/src/app.tsx
index eca78192cf..c64664e3bf 100644
--- a/console/src/main/resources/static/console-fe/src/app.tsx
+++ b/console/src/main/resources/static/console-fe/src/app.tsx
@@ -71,7 +71,7 @@ class App extends React.Component<AppPropsType, AppStateType>
{
getVersion = () => {
fetch('version.json').then(response =>
- response.json().then(json => this.setState({ ...this.state, version:
json.version }))
+ response.json().then(json => this.setState(prevState => ({ ...prevState,
version: json.version })))
);
};
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 b3fae1eb64..2ea2c9f66b 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
@@ -130,6 +130,7 @@ const enUs: ILocale = {
weight: 'Weight',
healthy: 'Healthy',
term: 'Term',
+ role: 'Role',
unit: 'Unit',
operations: 'Operations',
internal: 'Internal',
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 4673a6d6f8..c4b4f652f2 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
@@ -130,6 +130,7 @@ const zhCn: ILocale = {
weight: '权重',
healthy: '健康状态',
term: '任期',
+ role: '角色',
unit: '单元',
operations: '操作',
internal: '内部地址',
diff --git a/console/src/main/resources/static/console-fe/src/module.d.ts
b/console/src/main/resources/static/console-fe/src/module.d.ts
index c32f1f19e8..b1f68b6f71 100644
--- a/console/src/main/resources/static/console-fe/src/module.d.ts
+++ b/console/src/main/resources/static/console-fe/src/module.d.ts
@@ -30,6 +30,6 @@ declare module '*.svg' {
declare module 'lodash';
export interface GlobalProps {
- locale: ILocale;
+ locale: any;
history: History;
}
diff --git
a/console/src/main/resources/static/console-fe/src/pages/ClusterManager/ClusterManager.tsx
b/console/src/main/resources/static/console-fe/src/pages/ClusterManager/ClusterManager.tsx
index 0b38957762..ca625aa531 100644
---
a/console/src/main/resources/static/console-fe/src/pages/ClusterManager/ClusterManager.tsx
+++
b/console/src/main/resources/static/console-fe/src/pages/ClusterManager/ClusterManager.tsx
@@ -52,10 +52,11 @@ type ClusterManagerLocale = {
controlEndpoint?: string;
transactionEndpoint?: string;
metadataDialogTitle?: string;
+ role?: string;
};
type ClusterManagerState = {
- namespaceOptions: Map<string, { clusters: string[], clusterVgroups: {[key:
string]: string[]} }>;
+ namespaceOptions: Map<string, { clusters: string[], clusterVgroups: {[key:
string]: string[]}, clusterTypes: {[key: string]: string} }>;
clusters: Array<string>;
namespace?: string;
cluster?: string;
@@ -76,7 +77,7 @@ class ClusterManager extends React.Component<GlobalProps,
ClusterManagerState> {
};
state: ClusterManagerState = {
- namespaceOptions: new Map<string, { clusters: string[], clusterVgroups:
{[key: string]: string[]} }>(),
+ namespaceOptions: new Map<string, { clusters: string[], clusterVgroups:
{[key: string]: string[]}, clusterTypes: {[key: string]: string} }>(),
clusters: [],
clusterData: null,
loading: false,
@@ -94,14 +95,22 @@ class ClusterManager extends React.Component<GlobalProps,
ClusterManagerState> {
loadNamespaces = async () => {
try {
const namespaces = await fetchNamespaceV2();
- const namespaceOptions = new Map<string, { clusters: string[],
clusterVgroups: {[key: string]: string[]} }>();
+ const namespaceOptions = new Map<string, { clusters: string[],
clusterVgroups: {[key: string]: string[]}, clusterTypes: {[key: string]:
string} }>();
Object.keys(namespaces).forEach(namespaceKey => {
const namespaceData = namespaces[namespaceKey];
- const clusterVgroups: {[key: string]: string[]} =
namespaceData.clusterVgroups || {};
- const clusters = Object.keys(clusterVgroups);
+ const clustersData = namespaceData.clusters || {};
+ const clusterVgroups: {[key: string]: string[]} = {};
+ const clusterTypes: {[key: string]: string} = {};
+ Object.keys(clustersData).forEach(clusterName => {
+ const cluster = clustersData[clusterName];
+ clusterVgroups[clusterName] = cluster.vgroups || [];
+ clusterTypes[clusterName] = cluster.type || 'default';
+ });
+ const clusters = Object.keys(clustersData);
namespaceOptions.set(namespaceKey, {
clusters,
clusterVgroups,
+ clusterTypes,
});
});
if (namespaceOptions.size > 0) {
@@ -215,11 +224,13 @@ class ClusterManager extends React.Component<GlobalProps,
ClusterManagerState> {
};
render() {
- const { locale = {} } = this.props;
+ const { locale } = this.props;
const rawLocale = locale.ClusterManager;
const clusterManagerLocale: ClusterManagerLocale = typeof rawLocale ===
'object' && rawLocale !== null ? rawLocale : {};
- const { title, subTitle, selectNamespaceFilerPlaceholder,
selectClusterFilerPlaceholder, searchButtonLabel, unitName, members,
clusterType, view, unitDialogTitle, control, transaction, weight, healthy,
term, unit, operations, internal, version, metadata, controlEndpoint,
transactionEndpoint, metadataDialogTitle } = clusterManagerLocale;
+ const { title, subTitle, selectNamespaceFilerPlaceholder,
selectClusterFilerPlaceholder, searchButtonLabel, members, clusterType, view,
unitDialogTitle, control, transaction, weight, healthy, term, unit, operations,
internal, version, metadata, controlEndpoint, transactionEndpoint,
metadataDialogTitle, role } = clusterManagerLocale;
const unitData = this.state.clusterData ?
Object.entries(this.state.clusterData.unitData || {}) : [];
+ const { namespace } = this.state;
+ const namespaceData = namespace ?
this.state.namespaceOptions.get(namespace) : null;
return (
<Page
title={title || 'Cluster Manager'}
@@ -264,7 +275,7 @@ class ClusterManager extends React.Component<GlobalProps,
ClusterManagerState> {
</FormItem>
</Form>
{/* unit table */}
- <div>
+ <div style={{ marginTop: '20px' }}>
<Table dataSource={unitData} loading={this.state.loading}>
<Table.Column title={members || 'Members'} dataIndex="1"
cell={(val: any) => (val.namingInstanceList ? val.namingInstanceList.length :
0)} />
<Table.Column title={clusterType || 'Cluster Type'} cell={() =>
(this.state.clusterData ? this.state.clusterData.clusterType : '')} />
@@ -292,6 +303,7 @@ class ClusterManager extends React.Component<GlobalProps,
ClusterManagerState> {
<Table.Column title={weight || 'Weight'} dataIndex="weight" />
<Table.Column title={healthy || 'Healthy'} dataIndex="healthy"
cell={(val: boolean) => (val ? 'Yes' : 'No')} />
<Table.Column title={term || 'Term'} dataIndex="term" />
+ <Table.Column title={role || 'Role'} dataIndex="role" />
<Table.Column title={unit || 'Unit'} dataIndex="unit" />
<Table.Column title={version || 'Version'} dataIndex="version" />
<Table.Column title={metadata || 'Metadata'} dataIndex="metadata"
cell={(val: any) => (val ? <Button onClick={() =>
this.showMetadataDialog(val)}>View JSON</Button> : '')} />
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 075483aaf4..1dd04d0ba5 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
@@ -30,6 +30,7 @@ import {
} from '@alicloud/console-components';
import Actions from '@alicloud/console-components-actions';
import { withRouter } from 'react-router-dom';
+import { connect } from 'react-redux';
import Page from '@/components/Page';
import { GlobalProps } from '@/module';
import getData, {checkData, deleteData, GlobalLockParam } from
'@/service/globalLockInfo';
@@ -105,8 +106,12 @@ class GlobalLockInfo extends React.Component<GlobalProps,
GlobalLockInfoState> {
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);
+ const clustersData = namespaceData.clusters || {};
+ const clusterVgroups: {[key: string]: string[]} = {};
+ Object.keys(clustersData).forEach(clusterName => {
+ clusterVgroups[clusterName] = clustersData[clusterName].vgroups ||
[];
+ });
+ const clusters = Object.keys(clustersData);
namespaceOptions.set(namespaceKey, {
clusters,
clusterVgroups,
@@ -149,15 +154,15 @@ class GlobalLockInfo extends React.Component<GlobalProps,
GlobalLockInfoState> {
}
}
resetSearchFilter = () => {
- this.setState({
+ this.setState(prevState => ({
globalLockParam: {
// pagination info don`t reset
- pageSize: this.state.globalLockParam.pageSize,
- pageNum: this.state.globalLockParam.pageNum,
+ pageSize: prevState.globalLockParam.pageSize,
+ pageNum: prevState.globalLockParam.pageNum,
},
clusters: [],
vgroups: [],
- });
+ }));
}
search = () => {
@@ -276,10 +281,10 @@ class GlobalLockInfo extends React.Component<GlobalProps,
GlobalLockInfoState> {
}
deleteCell = (val: string, index: number, record: any) => {
- const {locale = {}} = this.props;
+ const { locale } = this.props;
const {
deleteGlobalLockTitle
- } = locale;
+ } = locale.GlobalLockInfo || {};
let width = getCurrentLanguage() === enUsKey ? '120px' : '80px'
return (
<Actions style={{width: width}}>
@@ -316,7 +321,8 @@ class GlobalLockInfo extends React.Component<GlobalProps,
GlobalLockInfoState> {
render() {
- const { locale = {} } = this.props;
+ const { locale } = this.props;
+ const globalLockInfo = locale.GlobalLockInfo || {};
const { title, subTitle, createTimeLabel,
inputFilterPlaceholder,
selectNamespaceFilerPlaceholder,
@@ -325,7 +331,7 @@ class GlobalLockInfo extends React.Component<GlobalProps,
GlobalLockInfoState> {
searchButtonLabel,
resetButtonLabel,
operateTitle,
- } = locale;
+ } = globalLockInfo;
return (
<Page
title={title}
@@ -453,4 +459,8 @@ class GlobalLockInfo extends React.Component<GlobalProps,
GlobalLockInfoState> {
}
}
-export default withRouter(ConfigProvider.config(GlobalLockInfo, {}));
+const mapStateToProps = (state: any) => ({
+ locale: state.locale.locale,
+});
+
+export default
ConfigProvider.config(withRouter(connect(mapStateToProps)(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 1b329a229a..5445347161 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
@@ -18,11 +18,11 @@ import React from 'react';
import { ConfigProvider, Table, Button, DatePicker, Form, Icon, Switch,
Pagination, Dialog, Input, Select, Message } from
'@alicloud/console-components';
import Actions, { LinkButton } from '@alicloud/console-components-actions';
import { withRouter } from 'react-router-dom';
+import { connect } from 'react-redux';
import Page from '@/components/Page';
import { GlobalProps } from '@/module';
import getData, { changeGlobalData, deleteBranchData, deleteGlobalData,
GlobalSessionParam, sendGlobalCommitOrRollback,
startBranchData, startGlobalData, stopBranchData, stopGlobalData,
forceDeleteGlobalData, forceDeleteBranchData, fetchNamespaceV2, addGroup,
changeGroup } from '@/service/transactionInfo';
-import PropTypes from 'prop-types';
import moment from 'moment';
import './index.scss';
@@ -47,7 +47,7 @@ type TransactionInfoState = {
xid : string;
currentBranchSession: Array<any>;
globalSessionParam : GlobalSessionParam;
- namespaceOptions: Map<string, { clusters: string[], clusterVgroups: {[key:
string]: string[]} }>;
+ namespaceOptions: Map<string, NamespaceData>;
clusters: Array<string>;
vgroups: Array<string>;
createVGroupDialogVisible: boolean;
@@ -57,14 +57,20 @@ type TransactionInfoState = {
targetNamespace: string;
targetClusters: Array<string>;
targetCluster: string;
+ targetUnits: Array<string>;
+ targetUnit: string;
originalNamespace: string;
originalClusters: Array<string>;
originalCluster: string;
originalVGroups: Array<string>;
createNamespace: string;
createCluster: string;
+ createUnits: Array<string>;
+ createUnit: string;
}
+type NamespaceData = { clusters: string[], clusterVgroups: {[key: string]:
string[]}, clusterUnits: {[key: string]: string[]}, clusterTypes: {[key:
string]: string} };
+
const statusList:Array<StatusType> = [
{
label: 'AsyncCommitting',
@@ -305,16 +311,11 @@ const warnning = new Map([
['SAGA', 'The force delete will only delete session in server.']])],
])
-const VGROUP_REFRESH_DELAY_MS = 5000;
+const VGROUP_REFRESH_DELAY_MS = 6000;
class TransactionInfo extends React.Component<GlobalProps,
TransactionInfoState> {
static displayName = 'TransactionInfo';
- static propTypes = {
- locale: PropTypes.object,
- history: PropTypes.object,
- };
-
state: TransactionInfoState = {
list: [],
total: 0,
@@ -327,7 +328,7 @@ class TransactionInfo extends React.Component<GlobalProps,
TransactionInfoState>
pageSize: 10,
pageNum: 1,
},
- namespaceOptions: new Map<string, { clusters: string[], clusterVgroups:
{[key: string]: string[]} }>(),
+ namespaceOptions: new Map<string, NamespaceData>(),
clusters: [],
vgroups: [],
createVGroupDialogVisible: false,
@@ -337,12 +338,16 @@ class TransactionInfo extends
React.Component<GlobalProps, TransactionInfoState>
targetNamespace: '',
targetClusters: [],
targetCluster: '',
+ targetUnits: [],
+ targetUnit: '',
originalNamespace: '',
originalClusters: [],
originalCluster: '',
originalVGroups: [],
createNamespace: '',
createCluster: '',
+ createUnits: [],
+ createUnit: '',
};
componentDidMount = () => {
// search once by default
@@ -351,14 +356,26 @@ class TransactionInfo extends
React.Component<GlobalProps, TransactionInfoState>
loadNamespaces = async () => {
try {
const namespaces = await fetchNamespaceV2();
- const namespaceOptions = new Map<string, { clusters: string[],
clusterVgroups: {[key: string]: string[]} }>();
+ const namespaceOptions = new Map<string, NamespaceData>();
+
Object.keys(namespaces).forEach(namespaceKey => {
const namespaceData = namespaces[namespaceKey];
- const clusterVgroups = (namespaceData.clusterVgroups || {}) as {[key:
string]: string[]};
- const clusters = Object.keys(clusterVgroups);
+ const clustersData = namespaceData.clusters || {};
+ const clusterVgroups: {[key: string]: string[]} = {};
+ const clusterUnits: {[key: string]: string[]} = {};
+ const clusterTypes: {[key: string]: string} = {};
+ Object.keys(clustersData).forEach(clusterName => {
+ const cluster = clustersData[clusterName];
+ clusterVgroups[clusterName] = cluster.vgroups || [];
+ clusterUnits[clusterName] = cluster.units || [];
+ clusterTypes[clusterName] = cluster.type || 'default';
+ });
+ const clusters = Object.keys(clustersData);
namespaceOptions.set(namespaceKey, {
clusters,
clusterVgroups,
+ clusterUnits,
+ clusterTypes,
});
});
if (namespaceOptions.size > 0) {
@@ -403,6 +420,8 @@ class TransactionInfo extends React.Component<GlobalProps,
TransactionInfoState>
search = () => {
this.setState({ loading: true });
+ const currentBranchSessionDialogVisible =
this.state.branchSessionDialogVisible;
+ const currentXid = this.state.xid;
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) {
@@ -429,8 +448,8 @@ class TransactionInfo extends React.Component<GlobalProps,
TransactionInfoState>
}
});
- if (this.state.branchSessionDialogVisible) {
- const currentBranchSession = data.data.find((item: any) => item.xid ==
this.state.xid)?.branchSessionVOs || [];
+ if (currentBranchSessionDialogVisible) {
+ const currentBranchSession = data.data.find((item: any) => item.xid ==
currentXid)?.branchSessionVOs || [];
this.setState({
list: data.data,
total: data.total,
@@ -535,7 +554,7 @@ class TransactionInfo extends React.Component<GlobalProps,
TransactionInfoState>
}
operateCell = (val: string, index: number, record: any) => {
- const { locale = {}, history } = this.props;
+ const { locale, history } = this.props;
const {
showBranchSessionTitle,
showGlobalLockTitle,
@@ -545,7 +564,7 @@ class TransactionInfo extends React.Component<GlobalProps,
TransactionInfoState>
startGlobalSessionTitle,
sendGlobalSessionTitle,
changeGlobalSessionTitle,
- } = locale;
+ } = locale?.TransactionInfo || {};
let width = getCurrentLanguage() === enUsKey ? '450px' : '420px'
let height = '120px';
return (
@@ -729,14 +748,14 @@ class TransactionInfo extends
React.Component<GlobalProps, TransactionInfoState>
}
branchSessionDialogOperateCell = (val: string, index: number, record: any)
=> {
- const { locale = {}, history } = this.props;
+ const { locale, history } = this.props;
const {
showGlobalLockTitle,
deleteBranchSessionTitle,
stopBranchSessionTitle,
startBranchSessionTitle,
forceDeleteBranchSessionTitle,
- } = locale;
+ } = locale.TransactionInfo || {};
let width = getCurrentLanguage() === enUsKey ? '500px' : '450px'
let height = '120px';
return (
@@ -892,12 +911,18 @@ class TransactionInfo extends
React.Component<GlobalProps, TransactionInfoState>
}
showCreateVGroupDialog = () => {
- this.setState(prevState => ({
- createVGroupDialogVisible: true,
- vGroupName: '',
- createNamespace: prevState.globalSessionParam.namespace || '',
- createCluster: prevState.globalSessionParam.cluster || '',
- }));
+ this.setState(prevState => {
+ const clusterUnits = prevState.globalSessionParam.namespace ?
prevState.namespaceOptions.get(prevState.globalSessionParam.namespace)?.clusterUnits
|| {} : {};
+ const units = prevState.globalSessionParam.cluster ?
clusterUnits[prevState.globalSessionParam.cluster] || [] : [];
+ return {
+ createVGroupDialogVisible: true,
+ vGroupName: '',
+ createNamespace: prevState.globalSessionParam.namespace || '',
+ createCluster: prevState.globalSessionParam.cluster || '',
+ createUnits: units,
+ createUnit: units.length > 0 ? units[0] : '',
+ };
+ });
}
closeCreateVGroupDialog = () => {
@@ -906,6 +931,8 @@ class TransactionInfo extends React.Component<GlobalProps,
TransactionInfoState>
vGroupName: '',
createNamespace: '',
createCluster: '',
+ createUnits: [],
+ createUnit: '',
});
}
@@ -916,6 +943,8 @@ class TransactionInfo extends React.Component<GlobalProps,
TransactionInfoState>
targetNamespace: '',
targetClusters: [],
targetCluster: '',
+ targetUnits: [],
+ targetUnit: '',
originalNamespace: '',
originalClusters: [],
originalCluster: '',
@@ -930,6 +959,8 @@ class TransactionInfo extends React.Component<GlobalProps,
TransactionInfoState>
targetNamespace: '',
targetClusters: [],
targetCluster: '',
+ targetUnits: [],
+ targetUnit: '',
originalNamespace: '',
originalClusters: [],
originalCluster: '',
@@ -938,14 +969,18 @@ class TransactionInfo extends
React.Component<GlobalProps, TransactionInfoState>
}
handleCreateVGroup = () => {
- const { locale = {} } = this.props;
- const { createVGroupErrorMessage, createVGroupSuccessMessage,
createVGroupFailMessage } = locale;
- const { createNamespace, createCluster, vGroupName } = this.state;
+ const { locale } = this.props;
+ const { createVGroupErrorMessage, createVGroupSuccessMessage,
createVGroupFailMessage } = locale.TransactionInfo || {};
+ const { createNamespace, createCluster, vGroupName, createUnit } =
this.state;
if (!createNamespace || !createCluster || !vGroupName.trim()) {
Message.error(createVGroupErrorMessage);
return;
}
- addGroup(createNamespace, createCluster, vGroupName.trim()).then(() => {
+
+ const clusterType =
this.state.namespaceOptions.get(createNamespace)?.clusterTypes[createCluster];
+ const unitName = clusterType !== 'default' ? createUnit : '';
+
+ addGroup(createNamespace, createCluster, vGroupName.trim(),
unitName).then(() => {
Message.success(createVGroupSuccessMessage);
this.closeCreateVGroupDialog();
// Delay 5 seconds before reloading namespaces to get the latest vgroup
list
@@ -959,14 +994,18 @@ class TransactionInfo extends
React.Component<GlobalProps, TransactionInfoState>
});
}
- handleChangeVGroup = () => {
- const { locale = {} } = this.props;
- const { changeVGroupSuccessMessage, changeVGroupFailMessage } = locale;
- const { selectedVGroup, targetNamespace, targetCluster } = this.state;
+ handleChangeVGroup = () => {
+ const { locale } = this.props;
+ const { changeVGroupSuccessMessage, changeVGroupFailMessage } =
locale.TransactionInfo || {};
+ const { selectedVGroup, targetNamespace, targetCluster, targetUnit } =
this.state;
if (!selectedVGroup || !targetNamespace || !targetCluster) {
return;
}
- changeGroup(targetNamespace, targetCluster, selectedVGroup, "").then(() =>
{
+
+ const targetClusterType =
this.state.namespaceOptions.get(targetNamespace)?.clusterTypes[targetCluster];
+ const unitName = targetClusterType !== 'default' ? targetUnit : '';
+
+ changeGroup(targetNamespace, targetCluster, selectedVGroup,
unitName).then(() => {
Message.success(changeVGroupSuccessMessage);
this.closeChangeVGroupDialog();
// Delay 5 seconds before reloading namespaces to get the latest vgroup
list
@@ -978,10 +1017,20 @@ class TransactionInfo extends
React.Component<GlobalProps, TransactionInfoState>
const displayMessage = backendMessage ? `${changeVGroupFailMessage}:
${backendMessage}` : changeVGroupFailMessage;
Message.error(displayMessage);
});
+ }
+
+ isChangeVGroupDisabled = (): boolean => {
+ const { selectedVGroup, targetNamespace, targetCluster, targetUnit,
namespaceOptions } = this.state;
+ if (!selectedVGroup || !targetNamespace || !targetCluster) {
+ return true;
+ }
+ const clusterType =
namespaceOptions.get(targetNamespace)?.clusterTypes[targetCluster];
+ return clusterType !== 'default' && !targetUnit;
}
render() {
- const { locale = {} } = this.props;
+ const { locale } = this.props;
+ const transactionInfo = locale.TransactionInfo || {};
const { title, subTitle, createTimeLabel,
selectFilerPlaceholder,
selectNamespaceFilerPlaceholder,
@@ -1013,7 +1062,7 @@ class TransactionInfo extends
React.Component<GlobalProps, TransactionInfoState>
selectTargetNamespacePlaceholder,
selectTargetClusterPlaceholder,
selectVGroupPlaceholder,
- } = locale;
+ } = transactionInfo;
return (
<Page
title={title}
@@ -1189,9 +1238,13 @@ class TransactionInfo extends
React.Component<GlobalProps, TransactionInfoState>
onChange={(value: string) => {
this.setState(prevState => {
const clusters = value ?
prevState.namespaceOptions.get(value)?.clusters || [] : [];
+ const clusterUnits =
prevState.namespaceOptions.get(value)?.clusterUnits || {};
+ const units = clusters.length > 0 ?
clusterUnits[clusters[0]] || [] : [];
return {
createNamespace: value,
createCluster: clusters.length > 0 ? clusters[0] : '',
+ createUnits: units,
+ createUnit: units.length > 0 ? units[0] : '',
};
});
}}
@@ -1203,12 +1256,33 @@ class TransactionInfo extends
React.Component<GlobalProps, TransactionInfoState>
<Select
placeholder={selectClusterFilerPlaceholder}
onChange={(value: string) => {
- this.setState({ createCluster: value });
+ this.setState(prevState => {
+ const namespaceData =
prevState.namespaceOptions.get(prevState.createNamespace);
+ const clusterUnits = namespaceData ?
namespaceData.clusterUnits : {};
+ const units = clusterUnits[value] || [];
+ return {
+ createCluster: value,
+ createUnits: units,
+ createUnit: units.length > 0 ? units[0] : '',
+ };
+ });
}}
dataSource={this.state.namespaceOptions.get(this.state.createNamespace)?.clusters.map(value
=> ({ label: value, value })) || []}
value={this.state.createCluster}
/>
</FormItem>
+
{this.state.namespaceOptions.get(this.state.createNamespace)?.clusterTypes[this.state.createCluster]
!== 'default' && (
+ <FormItem name="createUnit" label="Unit">
+ <Select
+ placeholder="Select unit"
+ onChange={(value: string) => {
+ this.setState({ createUnit: value });
+ }}
+ dataSource={this.state.createUnits.map(value => ({ label:
value, value }))}
+ value={this.state.createUnit}
+ />
+ </FormItem>
+ )}
<FormItem name="vGroupName" label={vGroupNameLabel}>
<Input
placeholder={createVGroupInputPlaceholder}
@@ -1290,7 +1364,9 @@ class TransactionInfo extends
React.Component<GlobalProps, TransactionInfoState>
return {
targetNamespace: value,
targetClusters: clusters,
- targetCluster: clusters.length > 0 ? clusters[0] : '',
+ targetCluster: '',
+ targetUnits: [],
+ targetUnit: '',
};
});
}}
@@ -1303,14 +1379,36 @@ class TransactionInfo extends
React.Component<GlobalProps, TransactionInfoState>
hasClear
placeholder={selectTargetClusterPlaceholder}
onChange={(value: string) => {
- this.setState({ targetCluster: value });
+ this.setState(prevState => {
+ const namespaceData =
prevState.namespaceOptions.get(prevState.targetNamespace);
+ const clusterUnits = namespaceData ?
namespaceData.clusterUnits : {};
+ const units = clusterUnits[value] || [];
+ return {
+ targetCluster: value,
+ targetUnits: units,
+ targetUnit: units.length > 0 ? units[0] : '',
+ };
+ });
}}
dataSource={this.state.targetClusters.map(value => ({ label:
value, value }))}
value={this.state.targetCluster}
/>
</FormItem>
+ {this.state.targetCluster &&
this.state.namespaceOptions.get(this.state.targetNamespace)?.clusterTypes[this.state.targetCluster]
!== 'default' && (
+ <FormItem name="targetUnit" label="Target Unit">
+ <Select
+ hasClear
+ placeholder="Select Target Unit"
+ onChange={(value: string) => {
+ this.setState({ targetUnit: value });
+ }}
+ dataSource={this.state.targetUnits.map(value => ({ label:
value, value }))}
+ value={this.state.targetUnit}
+ />
+ </FormItem>
+ )}
<FormItem>
- <Button type="primary" onClick={this.handleChangeVGroup}
disabled={!this.state.selectedVGroup || !this.state.targetNamespace ||
!this.state.targetCluster}>
+ <Button type="primary" onClick={this.handleChangeVGroup}
disabled={this.isChangeVGroupDisabled()}>
{confirmButtonLabel}
</Button>
</FormItem>
@@ -1321,4 +1419,8 @@ class TransactionInfo extends
React.Component<GlobalProps, TransactionInfoState>
}
}
-export default withRouter(ConfigProvider.config(TransactionInfo, {}));
+const mapStateToProps = (state: any) => ({
+ locale: state.locale.locale,
+});
+
+export default
ConfigProvider.config(withRouter(connect(mapStateToProps)(TransactionInfo)),
{});
diff --git
a/console/src/main/resources/static/console-fe/src/reducers/locale.ts
b/console/src/main/resources/static/console-fe/src/reducers/locale.ts
index 6e8f7dec64..23afb708e6 100644
--- a/console/src/main/resources/static/console-fe/src/reducers/locale.ts
+++ b/console/src/main/resources/static/console-fe/src/reducers/locale.ts
@@ -52,7 +52,7 @@ const getCurrentLanguage = (): string => {
return lang;
}
-const getCurrentLocaleObj = (): any => {
+const getCurrentLocaleObj = (): ILocale => {
let lang = getCurrentLanguage();
return lang === zhCnKey ? zhCN : enUS;
diff --git
a/console/src/main/resources/static/console-fe/src/service/clusterManager.ts
b/console/src/main/resources/static/console-fe/src/service/clusterManager.ts
index fc1a890720..b5f8a6e8f3 100644
--- a/console/src/main/resources/static/console-fe/src/service/clusterManager.ts
+++ b/console/src/main/resources/static/console-fe/src/service/clusterManager.ts
@@ -16,6 +16,7 @@
*/
import requestV2 from '@/utils/requestV2';
import request from '@/utils/request';
+import qs from 'qs';
export async function fetchNamespaceV2(): Promise<any> {
const result = await requestV2.get('/naming/namespace', {
@@ -31,3 +32,20 @@ export async function fetchClusterData(namespace: string,
clusterName: string):
});
return result;
}
+
+export async function postChangeGroup(
+ namespace: string,
+ clusterName: string,
+ vGroup: string,
+ unitName: string = '',
+): Promise<any> {
+ const params = { namespace, clusterName, unitName, vGroup };
+ const result = await request.post('/naming/changeGroup',
qs.stringify(params), {
+ headers: { 'Content-Type': 'application/x-www-form-urlencoded' }
+ });
+ return result;
+}
+
+export async function changeGroup(namespace: string, clusterName: string,
vGroup: string, unitName: string = ''): Promise<any> {
+ return postChangeGroup(namespace, clusterName, vGroup, unitName);
+}
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 da99ed5958..ca293cb772 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
@@ -16,6 +16,7 @@
*/
import request from '@/utils/request';
import requestV2 from '@/utils/requestV2';
+import qs from 'qs';
export type GlobalSessionParam = {
xid?: string,
@@ -248,28 +249,18 @@ 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,
- },
+export async function addGroup(namespace: string, clusterName: string, vGroup:
string, unitName: string = ''): Promise<any> {
+ const params = { namespace, clusterName, unitName, vGroup };
+ const result = await request.post('/naming/addGroup', qs.stringify(params), {
+ headers: { 'Content-Type': 'application/x-www-form-urlencoded' }
});
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,
- },
+export async function changeGroup(namespace: string, clusterName: string,
vGroup: string, unitName: string = ''): Promise<any> {
+ const params = { namespace, clusterName, unitName, vGroup };
+ const result = await request.post('/naming/changeGroup',
qs.stringify(params), {
+ headers: { 'Content-Type': 'application/x-www-form-urlencoded' }
});
return result;
}
diff --git
a/namingserver/src/main/java/org/apache/seata/namingserver/entity/bo/NamespaceData.java
b/namingserver/src/main/java/org/apache/seata/namingserver/entity/bo/NamespaceData.java
new file mode 100644
index 0000000000..cbf2c07a8b
--- /dev/null
+++
b/namingserver/src/main/java/org/apache/seata/namingserver/entity/bo/NamespaceData.java
@@ -0,0 +1,53 @@
+/*
+ * 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.bo;
+
+import java.util.Map;
+import java.util.Set;
+
+/**
+ * Business Object representing collected namespace data for v2 API.
+ * <p>
+ * This class encapsulates the collected data for namespaces, including
+ * clusters, vgroups, and their mappings for both RAFT and non-RAFT clusters.
+ */
+public class NamespaceData {
+ private final Map<String, Set<String>> clustersMap;
+ private final Map<String, Set<String>> vgroupsMap;
+ private final Map<String, Map<String, Set<String>>> clusterVgroupsMap;
+
+ public 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;
+ }
+
+ public Map<String, Set<String>> getClustersMap() {
+ return clustersMap;
+ }
+
+ public Map<String, Set<String>> getVgroupsMap() {
+ return vgroupsMap;
+ }
+
+ public Map<String, Map<String, Set<String>>> getClusterVgroupsMap() {
+ return clusterVgroupsMap;
+ }
+}
diff --git
a/namingserver/src/main/java/org/apache/seata/namingserver/entity/vo/v2/ClusterVO.java
b/namingserver/src/main/java/org/apache/seata/namingserver/entity/vo/v2/ClusterVO.java
new file mode 100644
index 0000000000..f322611251
--- /dev/null
+++
b/namingserver/src/main/java/org/apache/seata/namingserver/entity/vo/v2/ClusterVO.java
@@ -0,0 +1,59 @@
+/*
+ * 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.ArrayList;
+import java.util.List;
+
+/**
+ * Value Object representing cluster information for the v2 API.
+ * <p>
+ * This class encapsulates the information for a single cluster, including
+ * its associated vgroups, units, and cluster type.
+ */
+public class ClusterVO {
+
+ private List<String> vgroups = new ArrayList<>();
+
+ private List<String> units = new ArrayList<>();
+
+ private String type = "default";
+
+ public List<String> getVgroups() {
+ return vgroups;
+ }
+
+ public void setVgroups(List<String> vgroups) {
+ this.vgroups = vgroups;
+ }
+
+ public List<String> getUnits() {
+ return units;
+ }
+
+ public void setUnits(List<String> units) {
+ this.units = units;
+ }
+
+ public String getType() {
+ return type;
+ }
+
+ public void setType(String type) {
+ this.type = type;
+ }
+}
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
index 24019eb134..22e1ec6716 100644
---
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
@@ -17,20 +17,19 @@
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.
+ * This class provides a mapping between cluster names and their associated
cluster information.
* 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>Contains a {@code clusters} map, which maps cluster names to {@link
ClusterVO} objects containing detailed cluster information.</li>
* <li>May have a different structure or additional fields compared to the
original version.</li>
* </ul>
* <p>
@@ -39,13 +38,13 @@ import java.util.Map;
*/
public class NamespaceVO {
- private Map<String, List<String>> clusterVgroups = new HashMap<>();
+ private Map<String, ClusterVO> clusters = new HashMap<>();
- public Map<String, List<String>> getClusterVgroups() {
- return clusterVgroups;
+ public Map<String, ClusterVO> getClusters() {
+ return clusters;
}
- public void setClusterVgroups(Map<String, List<String>> clusterVgroups) {
- this.clusterVgroups = clusterVgroups;
+ public void setClusters(Map<String, ClusterVO> clusters) {
+ this.clusters = clusters;
}
}
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 eced4cb15d..a738d229a0 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
@@ -36,6 +36,7 @@ import org.apache.seata.common.util.HttpClientUtil;
import org.apache.seata.common.util.StringUtils;
import org.apache.seata.namingserver.entity.bo.ClusterBO;
import org.apache.seata.namingserver.entity.bo.NamespaceBO;
+import org.apache.seata.namingserver.entity.bo.NamespaceData;
import org.apache.seata.namingserver.entity.pojo.ClusterData;
import org.apache.seata.namingserver.entity.vo.NamespaceVO;
import org.apache.seata.namingserver.entity.vo.monitor.ClusterVO;
@@ -79,22 +80,6 @@ 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;
@@ -182,6 +167,25 @@ public class NamingManager {
public Result<String> createGroup(
String namespace, String vGroup, String clusterName, String
unitName, boolean checkExist) {
+ // If unitName is blank, find it from cluster
+ String actualUnitName = unitName;
+ if (StringUtils.isBlank(unitName)) {
+ Map<String, ClusterData> clusterDataMap =
namespaceClusterDataMap.get(namespace);
+ if (clusterDataMap != null) {
+ ClusterData clusterData = clusterDataMap.get(clusterName);
+ if (clusterData != null &&
!CollectionUtils.isEmpty(clusterData.getUnitData())) {
+ Optional<Map.Entry<String, Unit>> optionalEntry =
+
clusterData.getUnitData().entrySet().stream().findFirst();
+ if (optionalEntry.isPresent()) {
+ actualUnitName = optionalEntry.get().getKey();
+ }
+ }
+ }
+ }
+ if (StringUtils.isBlank(actualUnitName)) {
+ LOGGER.error("no available unit for namespace {} and cluster {}",
namespace, clusterName);
+ return new Result<>("400", "no available unit for cluster: " +
clusterName);
+ }
// Check if vGroup already exists
if (checkExist && vGroupMap.getIfPresent(vGroup) != null) {
LOGGER.error("vGroup {} already exists", vGroup);
@@ -206,7 +210,7 @@ public class NamingManager {
+ NamingServerConstants.HTTP_ADD_GROUP_SUFFIX;
HashMap<String, String> params = new HashMap<>();
params.put(CONSTANT_GROUP, vGroup);
- params.put(NamingServerConstants.CONSTANT_UNIT, unitName);
+ params.put(NamingServerConstants.CONSTANT_UNIT, actualUnitName);
Map<String, String> header = new HashMap<>();
header.put(HTTP.CONTENT_TYPE,
ContentType.APPLICATION_FORM_URLENCODED.getMimeType());
@@ -474,6 +478,11 @@ public class NamingManager {
public Result<String> changeGroup(String namespace, String vGroup, String
clusterName, String unitName) {
long changeTime = System.currentTimeMillis();
ConcurrentMap<String, NamespaceBO> namespaceMap = new
ConcurrentHashMap<>(vGroupMap.get(vGroup));
+ Result<String> res = createGroup(namespace, vGroup, clusterName,
unitName, false);
+ if (!res.isSuccess()) {
+ LOGGER.error("add vgroup failed! {}", res.getMessage());
+ return res;
+ }
Set<String> currentNamespaces = namespaceMap.keySet();
Map<String, Set<String>> namespaceClusters = new HashMap<>();
for (String currentNamespace : currentNamespaces) {
@@ -482,11 +491,6 @@ public class NamingManager {
new HashSet<>(
namespaceMap.get(currentNamespace).getClusterMap().keySet()));
}
- Result<String> res = createGroup(namespace, vGroup, clusterName,
unitName, false);
- if (!res.isSuccess()) {
- LOGGER.error("add vgroup failed!" + res.getMessage());
- return res;
- }
AtomicReference<Result<String>> result = new AtomicReference<>();
namespaceClusters.forEach((oldNamespace, clusters) -> {
for (String cluster : clusters) {
@@ -500,7 +504,8 @@ public class NamingManager {
if (optionalEntry.isPresent()) {
String unit = optionalEntry.get().getKey();
Unit unitData =
optionalEntry.get().getValue();
- result.set(removeGroup(unitData, vGroup,
cluster, oldNamespace, unitName));
+ result.set(removeGroup(
+ unitData, vGroup, cluster,
oldNamespace, unitData.getUnitName()));
notifyClusterChange(vGroup, namespace,
cluster, unit, changeTime);
}
}
@@ -515,16 +520,16 @@ public class NamingManager {
NamespaceData data = collectNamespaceData();
Map<String, NamespaceVO> namespaceVOs = new HashMap<>();
- if (data.vgroupsMap.isEmpty()) {
- data.clustersMap.forEach((namespace, clusters) -> {
+ if (data.getVgroupsMap().isEmpty()) {
+ data.getClustersMap().forEach((namespace, clusters) -> {
NamespaceVO namespaceVO = new NamespaceVO();
namespaceVO.setClusters(new ArrayList<>(clusters));
namespaceVOs.put(namespace, namespaceVO);
});
} else {
- data.vgroupsMap.forEach((namespace, vgroups) -> {
+ data.getVgroupsMap().forEach((namespace, vgroups) -> {
NamespaceVO namespaceVO =
namespaceVOs.computeIfAbsent(namespace, k -> new NamespaceVO());
- Set<String> clusters = data.clustersMap.get(namespace);
+ Set<String> clusters = data.getClustersMap().get(namespace);
namespaceVO.setClusters(new ArrayList<>(clusters != null ?
clusters : Collections.emptyList()));
namespaceVO.setVgroups(new ArrayList<>(vgroups));
});
@@ -539,17 +544,41 @@ public class NamingManager {
// Build NamespaceVOv2
Map<String, org.apache.seata.namingserver.entity.vo.v2.NamespaceVO>
namespaceVOs = new HashMap<>();
- data.clustersMap.forEach((namespace, clusters) -> {
+ data.getClustersMap().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);
+ Map<String, org.apache.seata.namingserver.entity.vo.v2.ClusterVO>
clusterVOMap = new HashMap<>();
+
clusters.forEach(cluster -> {
- Set<String> vgSet = clusterVgSet.get(cluster);
- clusterVgList.put(cluster, vgSet != null ? new
ArrayList<>(vgSet) : new ArrayList<>());
+ org.apache.seata.namingserver.entity.vo.v2.ClusterVO clusterVO
=
+ new
org.apache.seata.namingserver.entity.vo.v2.ClusterVO();
+
+ // Set units and type for this cluster
+ Map<String, ClusterData> clusterDataMap =
namespaceClusterDataMap.get(namespace);
+ String clusterType = "default";
+ List<String> unitNames = new ArrayList<>();
+ if (clusterDataMap != null) {
+ ClusterData clusterData = clusterDataMap.get(cluster);
+ if (clusterData != null) {
+ unitNames = clusterData.getUnitData().values().stream()
+ .map(Unit::getUnitName)
+ .collect(Collectors.toList());
+ clusterType = clusterData.getClusterType();
+ clusterVO.setType(clusterType);
+ }
+ }
+ clusterVO.setUnits(unitNames);
+
+ // Set vgroups (same logic for all cluster types)
+ Map<String, Set<String>> clusterVgSet =
+ data.getClusterVgroupsMap().get(namespace);
+ Set<String> vgSet = clusterVgSet != null ?
clusterVgSet.get(cluster) : null;
+ clusterVO.setVgroups(vgSet != null ? new ArrayList<>(vgSet) :
new ArrayList<>());
+
+ clusterVOMap.put(cluster, clusterVO);
});
- namespaceVO.setClusterVgroups(clusterVgList);
+ namespaceVO.setClusters(clusterVOMap);
namespaceVOs.put(namespace, namespaceVO);
});
@@ -563,8 +592,7 @@ public class NamingManager {
Map<String, Map<String, Set<String>>> clusterVgroupsMap = new
HashMap<>(); // namespace -> cluster -> vgroups
// Collect all namespaces
- Set<String> allNamespaces = new HashSet<>();
- allNamespaces.addAll(namespaceClusterDataMap.keySet());
+ Set<String> allNamespaces = new
HashSet<>(namespaceClusterDataMap.keySet());
vGroupMap.asMap().values().forEach(namespaceMap ->
allNamespaces.addAll(namespaceMap.keySet()));
// Initialize maps for all namespaces
@@ -587,8 +615,9 @@ public class NamingManager {
Set<String> vgroups = vgroupsMap.get(namespace);
vgroups.add(vGroup);
- // Build cluster to vgroups
+ // Build cluster to vgroups mapping
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);
diff --git a/namingserver/src/main/resources/application.yml
b/namingserver/src/main/resources/application.yml
index f1a54facf3..ea243e2401 100644
--- a/namingserver/src/main/resources/application.yml
+++ b/namingserver/src/main/resources/application.yml
@@ -14,7 +14,6 @@
# See the License for the specific language governing permissions and
# limitations under the License.
#
-
server:
port: 8081
spring:
diff --git
a/namingserver/src/test/java/org/apache/seata/namingserver/NamingControllerV2Test.java
b/namingserver/src/test/java/org/apache/seata/namingserver/NamingControllerV2Test.java
index a3ebf87832..81b1985a8d 100644
---
a/namingserver/src/test/java/org/apache/seata/namingserver/NamingControllerV2Test.java
+++
b/namingserver/src/test/java/org/apache/seata/namingserver/NamingControllerV2Test.java
@@ -75,9 +75,15 @@ class NamingControllerV2Test {
NamespaceVO namespaceVO = result.getData().get(namespace);
assertNotNull(namespaceVO);
- assertNotNull(namespaceVO.getClusterVgroups());
- assertTrue(namespaceVO.getClusterVgroups().containsKey(clusterName));
-
assertTrue(namespaceVO.getClusterVgroups().get(clusterName).contains(vGroup));
+ assertNotNull(namespaceVO.getClusters());
+ assertTrue(namespaceVO.getClusters().containsKey(clusterName));
+ org.apache.seata.namingserver.entity.vo.v2.ClusterVO clusterVO =
+ namespaceVO.getClusters().get(clusterName);
+ assertNotNull(clusterVO);
+ assertNotNull(clusterVO.getVgroups());
+ assertTrue(clusterVO.getVgroups().contains(vGroup));
+ assertNotNull(clusterVO.getUnits());
+ assertTrue(clusterVO.getUnits().contains(unitName));
// 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 1191cfc230..f50c84d638 100644
---
a/namingserver/src/test/java/org/apache/seata/namingserver/NamingManagerTest.java
+++
b/namingserver/src/test/java/org/apache/seata/namingserver/NamingManagerTest.java
@@ -383,9 +383,15 @@ class NamingManagerTest {
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));
+ assertNotNull(namespaceVO.getClusters());
+ assertTrue(namespaceVO.getClusters().containsKey(clusterName));
+ org.apache.seata.namingserver.entity.vo.v2.ClusterVO clusterVO =
+ namespaceVO.getClusters().get(clusterName);
+ assertNotNull(clusterVO);
+ assertNotNull(clusterVO.getVgroups());
+ assertTrue(clusterVO.getVgroups().contains(vGroup));
+ assertNotNull(clusterVO.getUnits());
+ assertTrue(clusterVO.getUnits().contains(unitName));
}
@Test
diff --git
a/server/src/main/java/org/apache/seata/server/controller/VGroupMappingController.java
b/server/src/main/java/org/apache/seata/server/controller/VGroupMappingController.java
index c60a3699b0..a1ca37b0da 100644
---
a/server/src/main/java/org/apache/seata/server/controller/VGroupMappingController.java
+++
b/server/src/main/java/org/apache/seata/server/controller/VGroupMappingController.java
@@ -18,11 +18,11 @@ package org.apache.seata.server.controller;
import org.apache.seata.common.metadata.Instance;
import org.apache.seata.common.result.Result;
-import org.apache.seata.config.Configuration;
-import org.apache.seata.config.ConfigurationFactory;
+import org.apache.seata.common.util.StringUtils;
import org.apache.seata.core.store.MappingDO;
import org.apache.seata.server.session.SessionHolder;
import org.apache.seata.server.store.VGroupMappingStoreManager;
+import org.springframework.beans.factory.annotation.Value;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
@@ -34,7 +34,8 @@ public class VGroupMappingController {
private VGroupMappingStoreManager vGroupMappingStoreManager;
- protected static final Configuration CONFIG =
ConfigurationFactory.getInstance();
+ @Value("${sessionMode:file}")
+ String sessionMode;
/**
* add vGroup in cluster
@@ -51,7 +52,9 @@ public class VGroupMappingController {
mappingDO.setUnit(unit);
mappingDO.setVGroup(vGroup);
boolean rst =
SessionHolder.getRootVGroupMappingManager().addVGroup(mappingDO);
- Instance.getInstance().setTerm(System.currentTimeMillis());
+ if (!StringUtils.equalsIgnoreCase("raft", sessionMode)) {
+ Instance.getInstance().setTerm(System.currentTimeMillis());
+ }
if (!rst) {
result.setCode("500");
result.setMessage("add vGroup failed!");
@@ -69,7 +72,9 @@ public class VGroupMappingController {
public Result<?> removeVGroup(@RequestParam String vGroup) {
Result<?> result = new Result<>();
boolean rst =
SessionHolder.getRootVGroupMappingManager().removeVGroup(vGroup);
- Instance.getInstance().setTerm(System.currentTimeMillis());
+ if (!StringUtils.equalsIgnoreCase("raft", sessionMode)) {
+ Instance.getInstance().setTerm(System.currentTimeMillis());
+ }
if (!rst) {
result.setCode("500");
result.setMessage("remove vGroup failed!");
diff --git
a/server/src/test/java/org/apache/seata/server/controller/VGroupMappingControllerTest.java
b/server/src/test/java/org/apache/seata/server/controller/VGroupMappingControllerTest.java
index 8759159f85..0ddd708e72 100644
---
a/server/src/test/java/org/apache/seata/server/controller/VGroupMappingControllerTest.java
+++
b/server/src/test/java/org/apache/seata/server/controller/VGroupMappingControllerTest.java
@@ -16,24 +16,124 @@
*/
package org.apache.seata.server.controller;
-import org.junit.jupiter.api.Disabled;
+import org.apache.seata.common.metadata.Instance;
+import org.apache.seata.common.result.Result;
+import org.apache.seata.core.store.MappingDO;
+import org.apache.seata.server.BaseSpringBootTest;
+import org.apache.seata.server.session.SessionHolder;
+import org.apache.seata.server.store.VGroupMappingStoreManager;
+import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
-import org.springframework.beans.factory.annotation.Autowired;
+import org.junit.jupiter.api.extension.ExtendWith;
+import org.mockito.ArgumentCaptor;
+import org.mockito.Mock;
+import org.mockito.MockedStatic;
+import org.mockito.Mockito;
+import org.mockito.junit.jupiter.MockitoExtension;
import org.springframework.boot.test.context.SpringBootTest;
-@Disabled
+import java.lang.reflect.Field;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
+
@SpringBootTest
-class VGroupMappingControllerTest {
- @Autowired
+@ExtendWith(MockitoExtension.class)
+class VGroupMappingControllerTest extends BaseSpringBootTest {
+
private VGroupMappingController vGroupMappingController;
+ @Mock
+ private VGroupMappingStoreManager vGroupMappingStoreManager;
+
+ private Instance instance;
+
+ @BeforeEach
+ void setUp() throws Exception {
+ vGroupMappingController = new VGroupMappingController();
+ instance = Instance.getInstance();
+ instance.setNamespace("default");
+ instance.setClusterName("default");
+ instance.setTerm(0);
+ setSessionMode("file");
+ }
+
+ @Test
+ void addVGroupShouldSetTerm_whenSessionModeIsNotRaft() throws Exception {
+ try (MockedStatic<SessionHolder> sessionHolderMock =
Mockito.mockStatic(SessionHolder.class)) {
+
when(vGroupMappingStoreManager.addVGroup(org.mockito.ArgumentMatchers.any(MappingDO.class)))
+ .thenReturn(true);
+
sessionHolderMock.when(SessionHolder::getRootVGroupMappingManager).thenReturn(vGroupMappingStoreManager);
+ long before = instance.getTerm();
+ Result<?> result = vGroupMappingController.addVGroup("vgroup",
"unit-a");
+ assertEquals(Result.SUCCESS_CODE, result.getCode());
+ assertEquals(Result.SUCCESS_MSG, result.getMessage());
+ assertTrue(instance.getTerm() >= before);
+ ArgumentCaptor<MappingDO> mappingCaptor =
ArgumentCaptor.forClass(MappingDO.class);
+
verify(vGroupMappingStoreManager).addVGroup(mappingCaptor.capture());
+ MappingDO mappingDO = mappingCaptor.getValue();
+ assertEquals("default", mappingDO.getNamespace());
+ assertEquals("default", mappingDO.getCluster());
+ assertEquals("unit-a", mappingDO.getUnit());
+ assertEquals("vgroup", mappingDO.getVGroup());
+ }
+ }
+
@Test
- void addVGroup() {
- vGroupMappingController.addVGroup("group1", "unit1");
+ void addVGroupShouldReturnErrorWhenStoreFails() throws Exception {
+ try (MockedStatic<SessionHolder> sessionHolderMock =
Mockito.mockStatic(SessionHolder.class)) {
+
when(vGroupMappingStoreManager.addVGroup(org.mockito.ArgumentMatchers.any(MappingDO.class)))
+ .thenReturn(false);
+
sessionHolderMock.when(SessionHolder::getRootVGroupMappingManager).thenReturn(vGroupMappingStoreManager);
+ Result<?> result = vGroupMappingController.addVGroup("vgroup",
"unit-a");
+ assertEquals("500", result.getCode());
+ assertEquals("add vGroup failed!", result.getMessage());
+ }
}
@Test
- void removeVGroup() {
- vGroupMappingController.removeVGroup("group1");
+ void removeVGroupShouldSetTerm_whenSessionModeIsNotRaft() throws Exception
{
+ try (MockedStatic<SessionHolder> sessionHolderMock =
Mockito.mockStatic(SessionHolder.class)) {
+
when(vGroupMappingStoreManager.removeVGroup("vgroup")).thenReturn(true);
+
sessionHolderMock.when(SessionHolder::getRootVGroupMappingManager).thenReturn(vGroupMappingStoreManager);
+ long before = instance.getTerm();
+ Result<?> result = vGroupMappingController.removeVGroup("vgroup");
+ assertEquals(Result.SUCCESS_CODE, result.getCode());
+ assertEquals(Result.SUCCESS_MSG, result.getMessage());
+ assertTrue(instance.getTerm() >= before);
+ verify(vGroupMappingStoreManager).removeVGroup("vgroup");
+ }
+ }
+
+ @Test
+ void removeVGroupShouldReturnErrorWhenStoreFails() throws Exception {
+ try (MockedStatic<SessionHolder> sessionHolderMock =
Mockito.mockStatic(SessionHolder.class)) {
+
when(vGroupMappingStoreManager.removeVGroup("vgroup")).thenReturn(false);
+
sessionHolderMock.when(SessionHolder::getRootVGroupMappingManager).thenReturn(vGroupMappingStoreManager);
+ Result<?> result = vGroupMappingController.removeVGroup("vgroup");
+ assertEquals("500", result.getCode());
+ assertEquals("remove vGroup failed!", result.getMessage());
+ }
+ }
+
+ @Test
+ void addVGroupShouldNotModifyTermWhenRaft() throws Exception {
+ setSessionMode("raft");
+ try (MockedStatic<SessionHolder> sessionHolderMock =
Mockito.mockStatic(SessionHolder.class)) {
+
when(vGroupMappingStoreManager.addVGroup(org.mockito.ArgumentMatchers.any(MappingDO.class)))
+ .thenReturn(true);
+
sessionHolderMock.when(SessionHolder::getRootVGroupMappingManager).thenReturn(vGroupMappingStoreManager);
+ long before = instance.getTerm();
+ vGroupMappingController.addVGroup("vgroup", "unit-a");
+ assertEquals(before, instance.getTerm());
+ }
+ }
+
+ private void setSessionMode(String mode) throws Exception {
+ Field sessionModeField =
VGroupMappingController.class.getDeclaredField("sessionMode");
+ sessionModeField.setAccessible(true);
+ sessionModeField.set(vGroupMappingController, mode);
}
}
---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]