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 41242611ab feature: support displaying cluster information in the
console (#7857)
41242611ab is described below
commit 41242611abc2de8c6bf09f9e496c4fb06f15af57
Author: funkye <[email protected]>
AuthorDate: Tue Dec 16 22:09:28 2025 +0800
feature: support displaying cluster information in the console (#7857)
---
changes/en-us/2.x.md | 1 +
changes/zh-cn/2.x.md | 3 +-
.../org/apache/seata/common/metadata/Instance.java | 21 ++
.../metadata/namingserver/NamingServerNode.java | 3 +-
.../common/metadata/namingserver/InstanceTest.java | 3 +
.../namingserver/NamingServerNodeTest.java | 3 +
.../main/resources/static/console-fe/src/app.tsx | 6 +-
.../static/console-fe/src/locales/en-us.ts | 28 +-
.../static/console-fe/src/locales/index.d.ts | 1 +
.../static/console-fe/src/locales/zh-cn.ts | 26 ++
.../resources/static/console-fe/src/module.d.ts | 4 +-
.../src/pages/ClusterManager/ClusterManager.tsx | 314 +++++++++++++++++++++
.../ClusterManager/index.scss} | 4 -
.../requestV2.ts => pages/ClusterManager/index.ts} | 5 +-
.../resources/static/console-fe/src/router.tsx | 2 +
.../src/{module.d.ts => service/clusterManager.ts} | 28 +-
.../static/console-fe/src/utils/request.ts | 4 +-
.../static/console-fe/src/utils/requestV2.ts | 2 +-
.../namingserver/controller/NamingController.java | 11 +
.../namingserver/entity/pojo/ClusterData.java | 2 +
.../seata/namingserver/manager/NamingManager.java | 11 +-
namingserver/src/main/resources/application.yml | 1 -
.../seata/namingserver/NamingControllerTest.java | 45 +++
.../seata/namingserver/NamingManagerTest.java | 41 ++-
.../instance/AbstractSeataInstanceStrategy.java | 2 +
.../instance/RaftServerInstanceStrategy.java | 5 +
26 files changed, 538 insertions(+), 38 deletions(-)
diff --git a/changes/en-us/2.x.md b/changes/en-us/2.x.md
index 2ead074bdf..adab1edbab 100644
--- a/changes/en-us/2.x.md
+++ b/changes/en-us/2.x.md
@@ -30,6 +30,7 @@ Add changes here for all PR submitted to the 2.x branch.
- [[#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
+- [[#7857](https://github.com/apache/incubator-seata/pull/7857)] support
displaying cluster information in the console
- [[#7826](https://github.com/apache/incubator-seata/pull/7826)] Support
HTTP/2 response handling for the Watch API in Server Raft mode
diff --git a/changes/zh-cn/2.x.md b/changes/zh-cn/2.x.md
index ebe102dc83..eb12b12194 100644
--- a/changes/zh-cn/2.x.md
+++ b/changes/zh-cn/2.x.md
@@ -28,8 +28,9 @@
- [[#7669](https://github.com/apache/incubator-seata/pull/7669)] 添加对 Jackson
序列化和反序列化 PostgreSQL 数组类型的支持
- [[#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流水綫
+- [[#7663](https://github.com/apache/incubator-seata/pull/7663)]
支持java25版本的CI流水线
- [[#7851](https://github.com/apache/incubator-seata/pull/7851)] 控制台支持事务分组管理能力
+- [[#7857](https://github.com/apache/incubator-seata/pull/7857)] 控制台支持集群信息展示
- [[#7826](https://github.com/apache/incubator-seata/pull/7826)] 在 Server Raft
模式下,为 Watch API 提供对 HTTP/2 响应处理的支持
diff --git
a/common/src/main/java/org/apache/seata/common/metadata/Instance.java
b/common/src/main/java/org/apache/seata/common/metadata/Instance.java
index 2862587d71..7acc4672da 100644
--- a/common/src/main/java/org/apache/seata/common/metadata/Instance.java
+++ b/common/src/main/java/org/apache/seata/common/metadata/Instance.java
@@ -31,10 +31,12 @@ public class Instance {
private String unit;
private Node.Endpoint control;
private Node.Endpoint transaction;
+ private Node.Endpoint internal;
private double weight = 1.0;
private boolean healthy = true;
private long term;
private long timestamp;
+ private String version;
private ClusterRole role = ClusterRole.MEMBER;
private Map<String, Object> metadata = new HashMap<>();
@@ -165,6 +167,15 @@ public class Instance {
}
}
+ public String getVersion() {
+ return version;
+ }
+
+ public void setVersion(String version) {
+ this.version = version;
+ }
+
+ @Override
public Instance clone() {
Instance instance = new Instance();
instance.setNamespace(namespace);
@@ -177,9 +188,19 @@ public class Instance {
instance.setTerm(term);
instance.setTimestamp(timestamp);
instance.setMetadata(metadata);
+ instance.setVersion(this.getVersion());
+ instance.setInternal(this.getInternal());
return instance;
}
+ public Node.Endpoint getInternal() {
+ return internal;
+ }
+
+ public void setInternal(Node.Endpoint internal) {
+ this.internal = internal;
+ }
+
private static class SingletonHolder {
private static final Instance SERVER_INSTANCE = new Instance();
private static final List<Instance> SERVER_INSTANCES = new
ArrayList<>();
diff --git
a/common/src/main/java/org/apache/seata/common/metadata/namingserver/NamingServerNode.java
b/common/src/main/java/org/apache/seata/common/metadata/namingserver/NamingServerNode.java
index b22a8cb620..cdef618bb3 100644
---
a/common/src/main/java/org/apache/seata/common/metadata/namingserver/NamingServerNode.java
+++
b/common/src/main/java/org/apache/seata/common/metadata/namingserver/NamingServerNode.java
@@ -17,6 +17,7 @@
package org.apache.seata.common.metadata.namingserver;
import org.apache.seata.common.metadata.Node;
+import org.apache.seata.common.util.StringUtils;
import java.util.Objects;
@@ -77,7 +78,7 @@ public class NamingServerNode extends Node {
NamingServerNode otherNode = (NamingServerNode) obj;
// other node is newer than me
- return otherNode.term > term;
+ return otherNode.term > term ||
!StringUtils.equals(otherNode.getVersion(), this.getVersion());
}
public void setWeight(double weight) {
diff --git
a/common/src/test/java/org/apache/seata/common/metadata/namingserver/InstanceTest.java
b/common/src/test/java/org/apache/seata/common/metadata/namingserver/InstanceTest.java
index b1312aa979..eb9e5d2204 100644
---
a/common/src/test/java/org/apache/seata/common/metadata/namingserver/InstanceTest.java
+++
b/common/src/test/java/org/apache/seata/common/metadata/namingserver/InstanceTest.java
@@ -60,7 +60,10 @@ class InstanceTest {
instance.setTimestamp(System.currentTimeMillis());
instance.setControl(new Node.Endpoint("1.1.1.1", 888));
instance.setTransaction(new Node.Endpoint("2.2.2.2", 999));
+ instance.setInternal(new Node.Endpoint("2.2.2.2", 1099));
assertEquals(instance.toJsonString(objectMapper),
objectMapper.writeValueAsString(instance));
+ assertEquals(
+ instance.clone().getInternal().getPort(),
instance.getInternal().getPort());
}
@Test
diff --git
a/common/src/test/java/org/apache/seata/common/metadata/namingserver/NamingServerNodeTest.java
b/common/src/test/java/org/apache/seata/common/metadata/namingserver/NamingServerNodeTest.java
index 1d9593c48c..e0c417a064 100644
---
a/common/src/test/java/org/apache/seata/common/metadata/namingserver/NamingServerNodeTest.java
+++
b/common/src/test/java/org/apache/seata/common/metadata/namingserver/NamingServerNodeTest.java
@@ -108,5 +108,8 @@ class NamingServerNodeTest {
Assertions.assertTrue(currentNode.isChanged(newerNode));
Assertions.assertFalse(currentNode.isChanged(olderNode));
Assertions.assertFalse(currentNode.isChanged(null));
+ newerNode = new NamingServerNode();
+ newerNode.setVersion("v1");
+ Assertions.assertTrue(currentNode.isChanged(newerNode));
}
}
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 d3bac4a24d..eca78192cf 100644
--- a/console/src/main/resources/static/console-fe/src/app.tsx
+++ b/console/src/main/resources/static/console-fe/src/app.tsx
@@ -78,7 +78,7 @@ class App extends React.Component<AppPropsType, AppStateType>
{
get menu() {
const { locale }: AppPropsType = this.props;
const { MenuRouter = {} } = locale;
- const { overview, transactionInfo, globalLockInfo,
sagaStatemachineDesigner } = MenuRouter;
+ const { transactionInfo, globalLockInfo, clusterManager,
sagaStatemachineDesigner } = MenuRouter;
return {
items: [
// {
@@ -93,6 +93,10 @@ class App extends React.Component<AppPropsType,
AppStateType> {
key: '/globallock/list',
label: globalLockInfo,
},
+ {
+ key: '/cluster/list',
+ label: clusterManager,
+ },
{
key: '/sagastatemachinedesigner',
label: sagaStatemachineDesigner,
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 c792ceec80..b3fae1eb64 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
@@ -22,6 +22,7 @@ const enUs: ILocale = {
overview: 'Overview',
transactionInfo: 'TransactionInfo',
globalLockInfo: 'GlobalLockInfo',
+ clusterManager: 'ClusterManager',
sagaStatemachineDesigner: 'SagaStatemachineDesigner',
},
Header: {
@@ -101,7 +102,7 @@ const enUs: ILocale = {
selectVGroupPlaceholder: 'Select vgroup',
},
GlobalLockInfo: {
- title: 'GlobalLockInfo',
+ title: 'Global Lock Info',
subTitle: 'list',
createTimeLabel: 'CreateTime',
inputFilterPlaceholder: 'Please enter filter criteria',
@@ -113,6 +114,31 @@ const enUs: ILocale = {
operateTitle: 'operate',
deleteGlobalLockTitle: 'Delete global lock',
},
+ ClusterManager: {
+ title: 'Cluster Manager',
+ subTitle: 'Cluster Node Management',
+ selectNamespaceFilerPlaceholder: 'Please select namespace',
+ selectClusterFilerPlaceholder: 'Please select cluster',
+ searchButtonLabel: 'Query',
+ unitName: 'Unit Name',
+ members: 'Members',
+ clusterType: 'Cluster Type',
+ view: 'View',
+ unitDialogTitle: 'Unit Details',
+ control: 'Control Address',
+ transaction: 'Transaction Address',
+ weight: 'Weight',
+ healthy: 'Healthy',
+ term: 'Term',
+ unit: 'Unit',
+ operations: 'Operations',
+ internal: 'Internal',
+ version: 'Version',
+ metadata: 'Metadata',
+ controlEndpoint: 'Control Endpoint',
+ transactionEndpoint: 'Transaction Endpoint',
+ metadataDialogTitle: 'Metadata',
+ },
codeMessage: {
200: 'The server successfully returned the requested data.',
201: 'New or modified data successful.',
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 2b43536e58..806b6f91f0 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,5 +25,6 @@ export interface ILocale {
Overview: ILocaleMap;
TransactionInfo: ILocaleMap;
GlobalLockInfo: ILocaleMap;
+ ClusterManager: 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 7ef23b08e3..4673a6d6f8 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
@@ -22,6 +22,7 @@ const zhCn: ILocale = {
overview: '概览',
transactionInfo: '事务信息',
globalLockInfo: '全局锁信息',
+ clusterManager: '集群管理',
sagaStatemachineDesigner: 'Saga状态机设计器',
},
Header: {
@@ -113,6 +114,31 @@ const zhCn: ILocale = {
operateTitle: '操作',
deleteGlobalLockTitle: '删除全局锁',
},
+ ClusterManager: {
+ title: '集群管理',
+ subTitle: '集群节点管理',
+ selectNamespaceFilerPlaceholder: '请选择命名空间',
+ selectClusterFilerPlaceholder: '请选择集群',
+ searchButtonLabel: '查询',
+ unitName: '单元名称',
+ members: '成员数',
+ clusterType: '集群类型',
+ view: '查看',
+ unitDialogTitle: '单元详情',
+ control: '控制地址',
+ transaction: '事务地址',
+ weight: '权重',
+ healthy: '健康状态',
+ term: '任期',
+ unit: '单元',
+ operations: '操作',
+ internal: '内部地址',
+ version: '版本',
+ metadata: '元数据',
+ controlEndpoint: '控制端点',
+ transactionEndpoint: '事务端点',
+ metadataDialogTitle: '元数据',
+ },
codeMessage: {
200: '服务器成功返回请求的数据。',
201: '新建或修改数据成功。',
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 d016f4ec0d..c32f1f19e8 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
@@ -17,7 +17,7 @@
/// <reference types="react" />
// tslint:disable
import { History } from 'history';
-import { ILocaleMap } from '@/locales';
+import { ILocale, ILocaleMap } from '@/locales';
declare const __mock__: boolean;
@@ -30,6 +30,6 @@ declare module '*.svg' {
declare module 'lodash';
export interface GlobalProps {
- locale: ILocaleMap;
+ locale: ILocale;
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
new file mode 100644
index 0000000000..0b38957762
--- /dev/null
+++
b/console/src/main/resources/static/console-fe/src/pages/ClusterManager/ClusterManager.tsx
@@ -0,0 +1,314 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+import React from 'react';
+import { ConfigProvider, Table, Button, Form, Icon, Dialog, Select, Message }
from '@alicloud/console-components';
+import Actions from '@alicloud/console-components-actions';
+import { withRouter } from 'react-router-dom';
+import { connect } from 'react-redux';
+import Page from '@/components/Page';
+import { GlobalProps } from '@/module';
+import { fetchNamespaceV2, fetchClusterData } from '@/service/clusterManager';
+import PropTypes from 'prop-types';
+
+import './index.scss';
+
+const FormItem = Form.Item;
+
+type ClusterManagerLocale = {
+ title?: string;
+ subTitle?: string;
+ selectNamespaceFilerPlaceholder?: string;
+ selectClusterFilerPlaceholder?: string;
+ searchButtonLabel?: string;
+ unitName?: string;
+ members?: string;
+ clusterType?: string;
+ view?: string;
+ unitDialogTitle?: string;
+ control?: string;
+ transaction?: string;
+ weight?: string;
+ healthy?: string;
+ term?: string;
+ unit?: string;
+ operations?: string;
+ internal?: string;
+ version?: string;
+ metadata?: string;
+ controlEndpoint?: string;
+ transactionEndpoint?: string;
+ metadataDialogTitle?: string;
+};
+
+type ClusterManagerState = {
+ namespaceOptions: Map<string, { clusters: string[], clusterVgroups: {[key:
string]: string[]} }>;
+ clusters: Array<string>;
+ namespace?: string;
+ cluster?: string;
+ clusterData: any; // ClusterData
+ loading: boolean;
+ unitDialogVisible: boolean;
+ selectedUnit: any; // Unit
+ selectedUnitName: string;
+ metadataDialogVisible: boolean;
+ selectedMetadata: any;
+};
+
+class ClusterManager extends React.Component<GlobalProps, ClusterManagerState>
{
+ static displayName = 'ClusterManager';
+
+ static propTypes = {
+ locale: PropTypes.object,
+ };
+
+ state: ClusterManagerState = {
+ namespaceOptions: new Map<string, { clusters: string[], clusterVgroups:
{[key: string]: string[]} }>(),
+ clusters: [],
+ clusterData: null,
+ loading: false,
+ unitDialogVisible: false,
+ selectedUnit: null,
+ selectedUnitName: '',
+ metadataDialogVisible: false,
+ selectedMetadata: null,
+ };
+
+ componentDidMount = () => {
+ this.loadNamespaces();
+ };
+
+ loadNamespaces = async () => {
+ try {
+ const namespaces = await fetchNamespaceV2();
+ const namespaceOptions = new Map<string, { clusters: string[],
clusterVgroups: {[key: string]: string[]} }>();
+ Object.keys(namespaces).forEach(namespaceKey => {
+ const namespaceData = namespaces[namespaceKey];
+ const clusterVgroups: {[key: string]: string[]} =
namespaceData.clusterVgroups || {};
+ const clusters = Object.keys(clusterVgroups);
+ namespaceOptions.set(namespaceKey, {
+ clusters,
+ clusterVgroups,
+ });
+ });
+ if (namespaceOptions.size > 0) {
+ const firstNamespace = Array.from(namespaceOptions.keys())[0];
+ const selectedNamespace = namespaceOptions.get(firstNamespace);
+ const firstCluster = selectedNamespace ? selectedNamespace.clusters[0]
: undefined;
+ this.setState(prevState => ({
+ ...prevState,
+ namespaceOptions,
+ namespace: firstNamespace,
+ cluster: firstCluster,
+ clusters: selectedNamespace ? selectedNamespace.clusters : [],
+ }), () => {
+ this.search();
+ });
+ } else {
+ this.setState(prevState => ({
+ ...prevState,
+ namespaceOptions,
+ }));
+ }
+ } catch (error) {
+ console.error('Failed to fetch namespaces:', error);
+ }
+ };
+
+ searchFilterOnChange = (key: string, val: string) => {
+ if (key === 'namespace') {
+ const selectedNamespace = this.state.namespaceOptions.get(val);
+ const clusters = selectedNamespace ? selectedNamespace.clusters : [];
+ const firstCluster = clusters.length > 0 ? clusters[0] : undefined;
+ this.setState(prevState => ({
+ ...prevState,
+ namespace: val,
+ cluster: firstCluster,
+ clusters,
+ }));
+ } else if (key === 'cluster') {
+ this.setState(prevState => ({
+ ...prevState,
+ cluster: val,
+ }));
+ }
+ };
+
+ search = () => {
+ const { namespace, cluster } = this.state;
+ if (!namespace || !cluster) {
+ Message.error('Please select namespace and cluster');
+ return;
+ }
+ this.setState(prevState => ({
+ ...prevState,
+ loading: true,
+ }));
+ fetchClusterData(namespace, cluster).then(data => {
+ if (data.success) {
+ this.setState(prevState => ({
+ ...prevState,
+ clusterData: data.data,
+ loading: false,
+ }));
+ } else {
+ Message.error(data.message || 'Failed to fetch cluster data');
+ this.setState(prevState => ({
+ ...prevState,
+ loading: false,
+ }));
+ }
+ }).catch(err => {
+ Message.error('Failed to fetch cluster data');
+ this.setState(prevState => ({
+ ...prevState,
+ loading: false,
+ }));
+ });
+ };
+
+ showUnitDialog = (unitName: string, unit: any) => {
+ this.setState(prevState => ({
+ ...prevState,
+ unitDialogVisible: true,
+ selectedUnit: unit,
+ selectedUnitName: unitName,
+ }));
+ };
+
+ closeUnitDialog = () => {
+ this.setState(prevState => ({
+ ...prevState,
+ unitDialogVisible: false,
+ selectedUnit: null,
+ selectedUnitName: '',
+ }));
+ };
+
+ showMetadataDialog = (metadata: any) => {
+ this.setState(prevState => ({
+ ...prevState,
+ metadataDialogVisible: true,
+ selectedMetadata: metadata,
+ }));
+ };
+
+ closeMetadataDialog = () => {
+ this.setState(prevState => ({
+ ...prevState,
+ metadataDialogVisible: false,
+ selectedMetadata: null,
+ }));
+ };
+
+ render() {
+ const { locale = {} } = this.props;
+ const rawLocale = locale.ClusterManager;
+ const clusterManagerLocale: ClusterManagerLocale = typeof rawLocale ===
'object' && rawLocale !== null ? rawLocale : {};
+ const { title, subTitle, selectNamespaceFilerPlaceholder,
selectClusterFilerPlaceholder, searchButtonLabel, unitName, members,
clusterType, view, unitDialogTitle, control, transaction, weight, healthy,
term, unit, operations, internal, version, metadata, controlEndpoint,
transactionEndpoint, metadataDialogTitle } = clusterManagerLocale;
+ const unitData = this.state.clusterData ?
Object.entries(this.state.clusterData.unitData || {}) : [];
+ return (
+ <Page
+ title={title || 'Cluster Manager'}
+ breadcrumbs={[
+ {
+ link: '/',
+ text: title || 'Cluster Manager',
+ },
+ {
+ text: subTitle || 'Manage Clusters',
+ },
+ ]}
+ >
+ {/* search form */}
+ <Form inline labelAlign="left">
+ <FormItem name="namespace" label="namespace">
+ <Select
+ hasClear
+ placeholder={selectNamespaceFilerPlaceholder || 'Select
namespace'}
+ onChange={(value: string) => {
+ this.searchFilterOnChange('namespace', value);
+ }}
+
dataSource={Array.from(this.state.namespaceOptions.keys()).map(key => ({ label:
key, value: key }))}
+ value={this.state.namespace}
+ />
+ </FormItem>
+ <FormItem name="cluster" label="cluster">
+ <Select
+ hasClear
+ placeholder={selectClusterFilerPlaceholder || 'Select cluster'}
+ onChange={(value: string) => {
+ this.searchFilterOnChange('cluster', value);
+ }}
+ dataSource={this.state.clusters.map(value => ({ label: value,
value }))}
+ value={this.state.cluster}
+ />
+ </FormItem>
+ <FormItem>
+ <Form.Submit onClick={this.search}>
+ <Icon type="search" />{searchButtonLabel || 'Search'}
+ </Form.Submit>
+ </FormItem>
+ </Form>
+ {/* unit table */}
+ <div>
+ <Table dataSource={unitData} loading={this.state.loading}>
+ <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 : '')} />
+ <Table.Column
+ title={operations || 'Operations'}
+ cell={(val: any, index: number, record: any) => {
+ return (
+ <Actions>
+ <Button onClick={() => this.showUnitDialog(record[0],
record[1])}>
+ {view || 'View'}
+ </Button>
+ </Actions>
+ );
+ }}
+ />
+ </Table>
+ </div>
+
+ {/* unit dialog */}
+ <Dialog visible={this.state.unitDialogVisible}
title={`${unitDialogTitle || 'Unit'}: ${this.state.selectedUnitName}`}
footer={false} onClose={this.closeUnitDialog} style={{ width: '80vw', height:
'80vh', overflow: 'auto' }}>
+ <Table dataSource={this.state.selectedUnit ?
this.state.selectedUnit.namingInstanceList || [] : []} style={{ overflow:
'auto' }}>
+ <Table.Column title={control || 'Control'} dataIndex="control"
cell={(val: any) => (val ? `${controlEndpoint || 'Control Endpoint'}:
${val.host}:${val.port}` : '')} />
+ <Table.Column title={transaction || 'Transaction'}
dataIndex="transaction" cell={(val: any) => (val ? `${transactionEndpoint ||
'Transaction Endpoint'}: ${val.host}:${val.port}` : '')} />
+ <Table.Column title={internal || 'Internal'} dataIndex="internal"
cell={(val: any) => (val ? `${val.host}:${val.port}` : '')} />
+ <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={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> : '')} />
+ </Table>
+ </Dialog>
+
+ {/* metadata dialog */}
+ <Dialog visible={this.state.metadataDialogVisible}
title={metadataDialogTitle || 'Metadata'} footer={false}
onClose={this.closeMetadataDialog} style={{ width: '80vw', height: '80vh',
overflow: 'auto' }}>
+ <pre>{JSON.stringify(this.state.selectedMetadata, null, 2)}</pre>
+ </Dialog>
+ </Page>
+ );
+ }
+}
+
+const mapStateToProps = (state: any) => ({
+ locale: state.locale.locale,
+});
+
+export default
connect(mapStateToProps)(withRouter(ConfigProvider.config(ClusterManager, {})));
diff --git
a/console/src/main/resources/static/console-fe/src/utils/requestV2.ts
b/console/src/main/resources/static/console-fe/src/pages/ClusterManager/index.scss
similarity index 87%
copy from console/src/main/resources/static/console-fe/src/utils/requestV2.ts
copy to
console/src/main/resources/static/console-fe/src/pages/ClusterManager/index.scss
index f467fc0c6b..6a9c064171 100644
--- a/console/src/main/resources/static/console-fe/src/utils/requestV2.ts
+++
b/console/src/main/resources/static/console-fe/src/pages/ClusterManager/index.scss
@@ -14,8 +14,4 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
-import { createRequest } from './request';
-const requestV2 = createRequest('api/v2');
-
-export default requestV2;
diff --git
a/console/src/main/resources/static/console-fe/src/utils/requestV2.ts
b/console/src/main/resources/static/console-fe/src/pages/ClusterManager/index.ts
similarity index 87%
copy from console/src/main/resources/static/console-fe/src/utils/requestV2.ts
copy to
console/src/main/resources/static/console-fe/src/pages/ClusterManager/index.ts
index f467fc0c6b..9b334587fd 100644
--- a/console/src/main/resources/static/console-fe/src/utils/requestV2.ts
+++
b/console/src/main/resources/static/console-fe/src/pages/ClusterManager/index.ts
@@ -14,8 +14,5 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
-import { createRequest } from './request';
+export { default } from './ClusterManager';
-const requestV2 = createRequest('api/v2');
-
-export default requestV2;
diff --git a/console/src/main/resources/static/console-fe/src/router.tsx
b/console/src/main/resources/static/console-fe/src/router.tsx
index d881b472d0..d3957a2999 100644
--- a/console/src/main/resources/static/console-fe/src/router.tsx
+++ b/console/src/main/resources/static/console-fe/src/router.tsx
@@ -18,10 +18,12 @@ import { HashRouter, Route, Switch, Redirect } from
'react-router-dom';
import Overview from '@/pages/Overview';
import TransactionInfo from '@/pages/TransactionInfo';
import GlobalLockInfo from './pages/GlobalLockInfo';
+import ClusterManager from './pages/ClusterManager';
export default [
// { path: '/', exact: true, render: () => <Redirect to="/Overview" /> },
// { path: '/Overview', component: Overview },
{ path: '/transaction/list', component: TransactionInfo },
{ path: '/globallock/list', component: GlobalLockInfo },
+ { path: '/cluster/list', component: ClusterManager },
];
diff --git a/console/src/main/resources/static/console-fe/src/module.d.ts
b/console/src/main/resources/static/console-fe/src/service/clusterManager.ts
similarity index 62%
copy from console/src/main/resources/static/console-fe/src/module.d.ts
copy to
console/src/main/resources/static/console-fe/src/service/clusterManager.ts
index d016f4ec0d..fc1a890720 100644
--- a/console/src/main/resources/static/console-fe/src/module.d.ts
+++ b/console/src/main/resources/static/console-fe/src/service/clusterManager.ts
@@ -14,22 +14,20 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
-/// <reference types="react" />
-// tslint:disable
-import { History } from 'history';
-import { ILocaleMap } from '@/locales';
+import requestV2 from '@/utils/requestV2';
+import request from '@/utils/request';
-declare const __mock__: boolean;
-
-
-declare module '*.svg' {
- const SvgIcon: React.ComponentClass<any>;
- export default SvgIcon;
+export async function fetchNamespaceV2(): Promise<any> {
+ const result = await requestV2.get('/naming/namespace', {
+ method: 'get',
+ });
+ return result.data;
}
-declare module 'lodash';
-
-export interface GlobalProps {
- locale: ILocaleMap;
- history: History;
+export async function fetchClusterData(namespace: string, clusterName:
string): Promise<any> {
+ const result = await request.get('/naming/clusterData', {
+ method: 'get',
+ params: { namespace, clusterName },
+ });
+ 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 d41e2119d5..1634022fe7 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
@@ -38,7 +38,7 @@ const createRequest = (baseURL: string, generalErrorMessage:
string = 'Request e
instance.interceptors.response.use(
(response: AxiosResponse): Promise<any> => {
const code = get(response, 'data.code');
- if (response.status === 200 && code === '200') {
+ if (response.status === 200 && String(code) === '200') {
return Promise.resolve(get(response, 'data'));
} else {
const currentLocale = getCurrentLocaleObj();
@@ -69,7 +69,7 @@ const createRequest = (baseURL: string, generalErrorMessage:
string = 'Request e
return instance;
};
-const request = createRequest('api/v1');
+const request = createRequest('/api/v1');
export { createRequest };
export default request;
diff --git
a/console/src/main/resources/static/console-fe/src/utils/requestV2.ts
b/console/src/main/resources/static/console-fe/src/utils/requestV2.ts
index f467fc0c6b..28f1dc3fa6 100644
--- a/console/src/main/resources/static/console-fe/src/utils/requestV2.ts
+++ b/console/src/main/resources/static/console-fe/src/utils/requestV2.ts
@@ -16,6 +16,6 @@
*/
import { createRequest } from './request';
-const requestV2 = createRequest('api/v2');
+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 a9e423cf23..f158f68265 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
@@ -20,6 +20,7 @@ import
org.apache.seata.common.metadata.namingserver.MetaResponse;
import org.apache.seata.common.metadata.namingserver.NamingServerNode;
import org.apache.seata.common.result.Result;
import org.apache.seata.common.result.SingleResult;
+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;
import org.apache.seata.namingserver.entity.vo.monitor.WatcherVO;
@@ -109,6 +110,16 @@ public class NamingController {
return namingManager.monitorCluster(namespace);
}
+ @GetMapping("/clusterData")
+ public SingleResult<ClusterData> getClusterData(@RequestParam String
namespace, @RequestParam String clusterName) {
+ ClusterData clusterData = namingManager.getClusterData(namespace,
clusterName);
+ if (clusterData != null) {
+ return SingleResult.success(clusterData);
+ } else {
+ return SingleResult.failure("Cluster not found");
+ }
+ }
+
@GetMapping("/discovery")
public MetaResponse discovery(@RequestParam String vGroup, @RequestParam
String namespace) {
return new MetaResponse(
diff --git
a/namingserver/src/main/java/org/apache/seata/namingserver/entity/pojo/ClusterData.java
b/namingserver/src/main/java/org/apache/seata/namingserver/entity/pojo/ClusterData.java
index 823e088cde..5612512277 100644
---
a/namingserver/src/main/java/org/apache/seata/namingserver/entity/pojo/ClusterData.java
+++
b/namingserver/src/main/java/org/apache/seata/namingserver/entity/pojo/ClusterData.java
@@ -16,6 +16,7 @@
*/
package org.apache.seata.namingserver.entity.pojo;
+import com.fasterxml.jackson.annotation.JsonIgnore;
import org.apache.seata.common.metadata.Cluster;
import org.apache.seata.common.metadata.Node;
import org.apache.seata.common.metadata.namingserver.NamingServerNode;
@@ -97,6 +98,7 @@ public class ClusterData {
}
}
+ @JsonIgnore
public List<Node> getInstanceList() {
return unitData.values().stream()
.map(Unit::getNamingInstanceList)
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 9a159c8997..f67d8ca26b 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
@@ -168,6 +168,14 @@ public class NamingManager {
return new ArrayList<>(clusterVOHashMap.values());
}
+ public ClusterData getClusterData(String namespace, String clusterName) {
+ Map<String, ClusterData> clusterDataMap =
namespaceClusterDataMap.get(namespace);
+ if (clusterDataMap != null) {
+ return clusterDataMap.get(clusterName);
+ }
+ return null;
+ }
+
public Result<String> createGroup(String namespace, String vGroup, String
clusterName, String unitName) {
return createGroup(namespace, vGroup, clusterName, unitName, true);
}
@@ -516,7 +524,8 @@ public class NamingManager {
} else {
data.vgroupsMap.forEach((namespace, vgroups) -> {
NamespaceVO namespaceVO =
namespaceVOs.computeIfAbsent(namespace, k -> new NamespaceVO());
- namespaceVO.setClusters(new
ArrayList<>(data.clustersMap.get(namespace)));
+ Set<String> clusters = data.clustersMap.get(namespace);
+ namespaceVO.setClusters(new ArrayList<>(clusters != null ?
clusters : Collections.emptyList()));
namespaceVO.setVgroups(new ArrayList<>(vgroups));
});
}
diff --git a/namingserver/src/main/resources/application.yml
b/namingserver/src/main/resources/application.yml
index 0ea1c8b5d2..ab636c40cc 100644
--- a/namingserver/src/main/resources/application.yml
+++ b/namingserver/src/main/resources/application.yml
@@ -17,7 +17,6 @@
server:
port: 8081
-
spring:
application:
name: seata-namingserver
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 20128d1da7..6029ee3e73 100644
---
a/namingserver/src/test/java/org/apache/seata/namingserver/NamingControllerTest.java
+++
b/namingserver/src/test/java/org/apache/seata/namingserver/NamingControllerTest.java
@@ -22,7 +22,9 @@ import
org.apache.seata.common.metadata.namingserver.MetaResponse;
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.namingserver.controller.NamingController;
+import org.apache.seata.namingserver.entity.pojo.ClusterData;
import org.apache.seata.namingserver.manager.NamingManager;
import org.junit.jupiter.api.Test;
import org.junit.runner.RunWith;
@@ -42,6 +44,8 @@ import java.util.concurrent.TimeUnit;
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.assertNull;
+import static org.junit.jupiter.api.Assertions.assertTrue;
import static org.junit.jupiter.api.Assertions.fail;
@RunWith(SpringRunner.class)
@@ -399,4 +403,45 @@ class NamingControllerTest {
fail("Test failed due to exception: " + e.getMessage());
}
}
+
+ @Test
+ void testGetClusterData() {
+ String clusterName = "test-cluster";
+ String namespace = "test-namespace";
+ String unitName = String.valueOf(UUID.randomUUID());
+ 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 HashMap<>();
+ vGroups.put("test-vGroup", unitName);
+ metadata.put(CONSTANT_GROUP, vGroups);
+ namingController.registerInstance(namespace, clusterName, unitName,
node);
+
+ SingleResult<ClusterData> result =
namingController.getClusterData(namespace, clusterName);
+ assertNotNull(result);
+ assertEquals("200", result.getCode());
+ assertEquals("success", result.getMessage());
+ assertNotNull(result.getData());
+ ClusterData clusterData = result.getData();
+ assertEquals(clusterName, clusterData.getClusterName());
+ assertNotNull(clusterData.getUnitData());
+ assertEquals(1, clusterData.getUnitData().size());
+ assertTrue(clusterData.getUnitData().containsKey(unitName));
+ Unit unit = clusterData.getUnitData().get(unitName);
+ assertNotNull(unit);
+ assertEquals(unitName, unit.getUnitName());
+ assertNotNull(unit.getNamingInstanceList());
+ assertEquals(1, unit.getNamingInstanceList().size());
+ Node instance = unit.getNamingInstanceList().get(0);
+ assertEquals("127.0.0.1", instance.getTransaction().getHost());
+ assertEquals(8091, instance.getTransaction().getPort());
+
+ // Test non-existent cluster
+ SingleResult<ClusterData> resultNotFound =
namingController.getClusterData(namespace, "non-existent-cluster");
+ assertNotNull(resultNotFound);
+ assertEquals("500", resultNotFound.getCode());
+ assertEquals("Cluster not found", resultNotFound.getMessage());
+ assertNull(resultNotFound.getData());
+ }
}
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 80e26d4b3b..1191cfc230 100644
---
a/namingserver/src/test/java/org/apache/seata/namingserver/NamingManagerTest.java
+++
b/namingserver/src/test/java/org/apache/seata/namingserver/NamingManagerTest.java
@@ -26,6 +26,7 @@ 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.pojo.ClusterData;
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;
@@ -48,10 +49,7 @@ 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.assertFalse;
-import static org.junit.jupiter.api.Assertions.assertNotNull;
-import static org.junit.jupiter.api.Assertions.assertTrue;
+import static org.junit.jupiter.api.Assertions.*;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.anyInt;
import static org.mockito.ArgumentMatchers.anyMap;
@@ -389,4 +387,39 @@ class NamingManagerTest {
assertTrue(namespaceVO.getClusterVgroups().containsKey(clusterName));
assertTrue(namespaceVO.getClusterVgroups().get(clusterName).contains(vGroup));
}
+
+ @Test
+ void testGetClusterData() {
+ String namespace = "test-namespace";
+ String clusterName = "test-cluster";
+ String unitName = UUID.randomUUID().toString();
+
+ NamingServerNode node = createTestNode("127.0.0.1", 8080, unitName);
+ boolean result = namingManager.registerInstance(node, namespace,
clusterName, unitName);
+
+ assertTrue(result);
+
+ ClusterData clusterData = namingManager.getClusterData(namespace,
clusterName);
+ assertNotNull(clusterData);
+ assertEquals(clusterName, clusterData.getClusterName());
+ assertNotNull(clusterData.getUnitData());
+ assertEquals(1, clusterData.getUnitData().size());
+ assertTrue(clusterData.getUnitData().containsKey(unitName));
+ Unit unit = clusterData.getUnitData().get(unitName);
+ assertNotNull(unit);
+ assertEquals(unitName, unit.getUnitName());
+ assertNotNull(unit.getNamingInstanceList());
+ assertEquals(1, unit.getNamingInstanceList().size());
+ Node instance = unit.getNamingInstanceList().get(0);
+ assertEquals("127.0.0.1", instance.getTransaction().getHost());
+ assertEquals(8080, instance.getTransaction().getPort());
+
+ // Test non-existent cluster
+ ClusterData notFound = namingManager.getClusterData(namespace,
"non-existent-cluster");
+ assertNull(notFound);
+
+ // Test non-existent namespace
+ ClusterData notFoundNamespace =
namingManager.getClusterData("non-existent-namespace", clusterName);
+ assertNull(notFoundNamespace);
+ }
}
diff --git
a/server/src/main/java/org/apache/seata/server/instance/AbstractSeataInstanceStrategy.java
b/server/src/main/java/org/apache/seata/server/instance/AbstractSeataInstanceStrategy.java
index 329d3e1777..00d8a8eef3 100644
---
a/server/src/main/java/org/apache/seata/server/instance/AbstractSeataInstanceStrategy.java
+++
b/server/src/main/java/org/apache/seata/server/instance/AbstractSeataInstanceStrategy.java
@@ -18,6 +18,7 @@ package org.apache.seata.server.instance;
import org.apache.seata.common.metadata.Instance;
import org.apache.seata.common.thread.NamedThreadFactory;
+import org.apache.seata.core.protocol.Version;
import org.apache.seata.server.session.SessionHolder;
import org.apache.seata.server.store.VGroupMappingStoreManager;
import
org.apache.seata.spring.boot.autoconfigure.properties.registry.RegistryNamingServerProperties;
@@ -69,6 +70,7 @@ public abstract class AbstractSeataInstanceStrategy
implements SeataInstanceStra
return;
}
Instance instance = serverInstanceInit();
+ instance.setVersion(Version.getCurrent());
if (init.compareAndSet(false, true)) {
VGroupMappingStoreManager vGroupMappingStoreManager =
SessionHolder.getRootVGroupMappingManager();
// load vgroup mapping relationship
diff --git
a/server/src/main/java/org/apache/seata/server/instance/RaftServerInstanceStrategy.java
b/server/src/main/java/org/apache/seata/server/instance/RaftServerInstanceStrategy.java
index 0bd1863bd1..7c20a40800 100644
---
a/server/src/main/java/org/apache/seata/server/instance/RaftServerInstanceStrategy.java
+++
b/server/src/main/java/org/apache/seata/server/instance/RaftServerInstanceStrategy.java
@@ -16,6 +16,7 @@
*/
package org.apache.seata.server.instance;
+import com.alipay.sofa.jraft.entity.PeerId;
import org.apache.seata.common.XID;
import org.apache.seata.common.holder.ObjectHolder;
import org.apache.seata.common.metadata.ClusterRole;
@@ -74,6 +75,10 @@ public class RaftServerInstanceStrategy extends
AbstractSeataInstanceStrategy
// load node Endpoint
instance.setControl(new Node.Endpoint(XID.getIpAddress(),
serverProperties.getPort(), "http"));
+ PeerId peerId =
+
RaftServerManager.getRaftServer(raftProperties.getGroup()).getServerId();
+ instance.setInternal(new Node.Endpoint(peerId.getIp(),
peerId.getPort(), "raft"));
+
// load metadata
for (PropertySource<?> propertySource :
environment.getPropertySources()) {
if (propertySource instanceof EnumerablePropertySource) {
---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]