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

lizhimins pushed a commit to branch rocketmq-studio
in repository https://gitbox.apache.org/repos/asf/rocketmq-dashboard.git


The following commit(s) were added to refs/heads/rocketmq-studio by this push:
     new 73cf84aa feat: route and audit cluster runtime operations by selected 
instance (#1244)
73cf84aa is described below

commit 73cf84aa3f93424f21881c9eae565667b010ce82
Author: aias00 <[email protected]>
AuthorDate: Mon Aug 10 19:05:18 2026 +0800

    feat: route and audit cluster runtime operations by selected instance 
(#1244)
    
    * feat: route cluster runtime operations by selected instance
    
    * test: preserve legacy cluster runtime paths
    
    * test: cover instance-scoped cluster operations
    
    * chore: include instance in cluster list logs
    
    * [ISSUE #1262] Populate runtime audit cluster IDs
    
    * test: bind cluster controller mocks to instance context
---
 .../studio/cluster/broker/ClusterController.java   | 10 +++--
 .../studio/cluster/broker/ClusterProvider.java     |  8 ++++
 .../studio/cluster/broker/ClusterService.java      | 50 ++++++++++++++++++++--
 .../studio/cluster/config/UpdateConfigDTO.java     |  2 +
 .../rocketmq/studio/ops/audit/AuditService.java    |  5 +++
 .../apache/RocketMQBrokerConfigService.java        | 23 ++++++++--
 .../provider/apache/RocketMQClusterProvider.java   | 30 ++++++++++---
 .../cluster/broker/ClusterControllerTest.java      |  7 +--
 .../studio/cluster/broker/ClusterServiceTest.java  | 42 +++++++++++++++++-
 .../studio/ops/audit/AuditServiceTest.java         | 10 +++++
 .../apache/RocketMQBrokerConfigServiceTest.java    | 23 ++++++++--
 .../apache/RocketMQClusterProviderTest.java        |  3 +-
 web/src/api/cluster.ts                             | 16 ++++---
 web/src/pages/cluster/index.tsx                    | 30 ++++++++++++-
 web/src/services/clusterService.ts                 | 12 +++---
 15 files changed, 235 insertions(+), 36 deletions(-)

diff --git 
a/server/src/main/java/org/apache/rocketmq/studio/cluster/broker/ClusterController.java
 
b/server/src/main/java/org/apache/rocketmq/studio/cluster/broker/ClusterController.java
index 7a72a7bc..1525c12a 100644
--- 
a/server/src/main/java/org/apache/rocketmq/studio/cluster/broker/ClusterController.java
+++ 
b/server/src/main/java/org/apache/rocketmq/studio/cluster/broker/ClusterController.java
@@ -28,6 +28,7 @@ import org.springframework.web.bind.annotation.PathVariable;
 import org.springframework.web.bind.annotation.PostMapping;
 import org.springframework.web.bind.annotation.RequestBody;
 import org.springframework.web.bind.annotation.RequestMapping;
+import org.springframework.web.bind.annotation.RequestParam;
 import org.springframework.web.bind.annotation.RestController;
 
 import java.util.List;
@@ -42,8 +43,8 @@ public class ClusterController {
     private final ClusterConnectionService clusterConnectionService;
 
     @GetMapping
-    public Result<List<ClusterVO>> listClusters() {
-        return Result.ok(clusterService.listClusters());
+    public Result<List<ClusterVO>> listClusters(@RequestParam(required = 
false) String instanceId) {
+        return Result.ok(clusterService.listClusters(instanceId));
     }
 
     @PostMapping("/test-connection")
@@ -52,8 +53,9 @@ public class ClusterController {
     }
 
     @GetMapping("/{id}")
-    public Result<ClusterVO> getCluster(@PathVariable String id) {
-        return Result.ok(clusterService.getCluster(id));
+    public Result<ClusterVO> getCluster(@PathVariable String id,
+                                        @RequestParam(required = false) String 
instanceId) {
+        return Result.ok(clusterService.getCluster(id, instanceId));
     }
 
     @PostMapping("/config/update")
diff --git 
a/server/src/main/java/org/apache/rocketmq/studio/cluster/broker/ClusterProvider.java
 
b/server/src/main/java/org/apache/rocketmq/studio/cluster/broker/ClusterProvider.java
index c7c583f0..4ce5228c 100644
--- 
a/server/src/main/java/org/apache/rocketmq/studio/cluster/broker/ClusterProvider.java
+++ 
b/server/src/main/java/org/apache/rocketmq/studio/cluster/broker/ClusterProvider.java
@@ -23,5 +23,13 @@ public interface ClusterProvider {
 
     List<ClusterVO> discoverClusters();
 
+    default List<ClusterVO> discoverClusters(String instanceId) {
+        return discoverClusters();
+    }
+
     ClusterVO refreshClusterDetail(String clusterId);
+
+    default ClusterVO refreshClusterDetail(String clusterId, String 
instanceId) {
+        return refreshClusterDetail(clusterId);
+    }
 }
diff --git 
a/server/src/main/java/org/apache/rocketmq/studio/cluster/broker/ClusterService.java
 
b/server/src/main/java/org/apache/rocketmq/studio/cluster/broker/ClusterService.java
index 8d85d49e..16570fe7 100644
--- 
a/server/src/main/java/org/apache/rocketmq/studio/cluster/broker/ClusterService.java
+++ 
b/server/src/main/java/org/apache/rocketmq/studio/cluster/broker/ClusterService.java
@@ -60,6 +60,16 @@ public class ClusterService {
         return List.of();
     }
 
+    public List<ClusterVO> listClusters(String instanceId) {
+        log.info("Listing clusters for instance: {}", instanceId);
+        List<ClusterVO> discovered = 
clusterProvider.discoverClusters(instanceId);
+        if (discovered != null && !discovered.isEmpty()) {
+            discovered.forEach(cluster -> enrichWithLiveConfig(cluster, 
instanceId));
+            return discovered;
+        }
+        return List.of();
+    }
+
     public ClusterVO getCluster(String id) {
         log.info("Getting cluster detail: {}", id);
         ClusterVO live = clusterProvider.refreshClusterDetail(id);
@@ -70,17 +80,31 @@ public class ClusterService {
         throw new BusinessException(503, "Cluster details are unavailable: " + 
id);
     }
 
+    public ClusterVO getCluster(String id, String instanceId) {
+        log.info("Getting cluster detail: {}", id);
+        ClusterVO live = clusterProvider.refreshClusterDetail(id, instanceId);
+        if (live != null) {
+            enrichWithLiveConfig(live, instanceId);
+            return live;
+        }
+        throw new BusinessException(503, "Cluster details are unavailable: " + 
id);
+    }
+
     /**
      * Attach live broker configuration (read from the first reachable master 
broker via the
      * admin API) to a discovered cluster. Falls back to the persisted config, 
if any, when the
      * live read is unavailable.
      */
     private void enrichWithLiveConfig(ClusterVO cluster) {
+        enrichWithLiveConfig(cluster, null);
+    }
+
+    private void enrichWithLiveConfig(ClusterVO cluster, String instanceId) {
         if (cluster.getBrokers() != null) {
             for (BrokerVO broker : cluster.getBrokers()) {
                 if (broker.getAddr() != null && !broker.getAddr().isEmpty()) {
                     try {
-                        
cluster.setConfig(brokerConfigService.getBrokerConfig(broker.getAddr()));
+                        
cluster.setConfig(brokerConfigService.getBrokerConfig(broker.getAddr(), 
instanceId));
                         return;
                     } catch (Exception e) {
                         log.warn("Failed to read live config from broker {}: 
{}",
@@ -97,7 +121,7 @@ public class ClusterService {
     public ClusterConfigUpdateResultVO updateClusterConfig(UpdateConfigDTO 
command) {
         log.info("Updating cluster config for: {}", command.getId());
         requireMatchingDefaultQueueNums(command);
-        ClusterVO cluster = resolveCluster(command.getId());
+        ClusterVO cluster = resolveCluster(command.getId(), 
command.getInstanceId());
 
         ClusterConfigVO config = copyConfig(cluster.getConfig());
         applyConfig(command, config);
@@ -112,7 +136,12 @@ public class ClusterService {
                     continue;
                 }
                 try {
-                    brokerConfigService.updateBrokerConfig(address, 
command.getId(), brokerProps);
+                    if (command.getInstanceId() == null || 
command.getInstanceId().isBlank()) {
+                        brokerConfigService.updateBrokerConfig(address, 
command.getId(), brokerProps);
+                    } else {
+                        brokerConfigService.updateBrokerConfig(
+                                address, command.getId(), 
command.getInstanceId(), brokerProps);
+                    }
                     successfulBrokers.add(address);
                 } catch (Exception e) {
                     failedBrokers.add(BrokerConfigUpdateFailureVO.builder()
@@ -190,7 +219,7 @@ public class ClusterService {
                 .map(failure -> failure.getAddress() + ": " + 
failure.getMessage())
                 .toList();
         try {
-            auditService.record("UPDATE_CLUSTER_CONFIG", "CLUSTER:" + 
clusterId, detail, status.name());
+            auditService.record("UPDATE_CLUSTER_CONFIG", "CLUSTER:" + 
clusterId, clusterId, detail, status.name());
         } catch (Exception e) {
             log.warn("Failed to record cluster config update audit for {}: 
{}", clusterId, e.getMessage());
         }
@@ -206,6 +235,19 @@ public class ClusterService {
                 .orElseThrow(() -> new BusinessException(404, "Cluster not 
found: " + clusterId));
     }
 
+    private ClusterVO resolveCluster(String clusterId, String instanceId) {
+        if (instanceId == null || instanceId.isBlank()) {
+            return resolveCluster(clusterId);
+        }
+        ClusterVO live = clusterProvider.refreshClusterDetail(clusterId, 
instanceId);
+        if (live != null) {
+            enrichWithLiveConfig(live, instanceId);
+            return live;
+        }
+        return clusterRepository.findById(clusterId)
+                .orElseThrow(() -> new BusinessException(404, "Cluster not 
found: " + clusterId));
+    }
+
     private ClusterConfigVO copyConfig(ClusterConfigVO config) {
         if (config == null) {
             return new ClusterConfigVO();
diff --git 
a/server/src/main/java/org/apache/rocketmq/studio/cluster/config/UpdateConfigDTO.java
 
b/server/src/main/java/org/apache/rocketmq/studio/cluster/config/UpdateConfigDTO.java
index a23d0c67..b9e9e1c5 100644
--- 
a/server/src/main/java/org/apache/rocketmq/studio/cluster/config/UpdateConfigDTO.java
+++ 
b/server/src/main/java/org/apache/rocketmq/studio/cluster/config/UpdateConfigDTO.java
@@ -32,6 +32,8 @@ public class UpdateConfigDTO {
     @NotBlank(message = "id is required")
     private String id;
 
+    private String instanceId;
+
     private String flushDiskType;
     private Boolean autoCreateTopicEnable;
     private Boolean autoCreateSubscriptionGroup;
diff --git 
a/server/src/main/java/org/apache/rocketmq/studio/ops/audit/AuditService.java 
b/server/src/main/java/org/apache/rocketmq/studio/ops/audit/AuditService.java
index f9a733b3..4179ffeb 100644
--- 
a/server/src/main/java/org/apache/rocketmq/studio/ops/audit/AuditService.java
+++ 
b/server/src/main/java/org/apache/rocketmq/studio/ops/audit/AuditService.java
@@ -85,11 +85,16 @@ public class AuditService {
 
 
     public void record(String operationType, String target, String detail, 
String result) {
+        record(operationType, target, null, detail, result);
+    }
+
+    public void record(String operationType, String target, String clusterId, 
String detail, String result) {
         AuditRecordVO record = AuditRecordVO.builder()
                 .timestamp(LocalDateTime.now())
                 .operator(AuthenticatedUserContext.currentUsernameOrSystem())
                 .operationType(operationType)
                 .target(target)
+                .clusterId(clusterId)
                 .detail(detail)
                 .result(result)
                 .build();
diff --git 
a/server/src/main/java/org/apache/rocketmq/studio/provider/apache/RocketMQBrokerConfigService.java
 
b/server/src/main/java/org/apache/rocketmq/studio/provider/apache/RocketMQBrokerConfigService.java
index 44010a09..2452120d 100644
--- 
a/server/src/main/java/org/apache/rocketmq/studio/provider/apache/RocketMQBrokerConfigService.java
+++ 
b/server/src/main/java/org/apache/rocketmq/studio/provider/apache/RocketMQBrokerConfigService.java
@@ -17,6 +17,7 @@
 package org.apache.rocketmq.studio.provider.apache;
 
 import org.apache.rocketmq.studio.cluster.broker.MqAdminExtFactory;
+import org.apache.rocketmq.studio.cluster.broker.RuntimeAdminClientResolver;
 import org.apache.rocketmq.studio.cluster.config.ClusterConfigVO;
 import org.apache.rocketmq.studio.common.domain.enums.FlushDiskType;
 import org.apache.rocketmq.studio.common.exception.BusinessException;
@@ -35,13 +36,18 @@ public class RocketMQBrokerConfigService {
 
     private final MqAdminExtFactory adminFactory;
     private final RocketMQProperties properties;
+    private final RuntimeAdminClientResolver runtimeAdminClientResolver;
     private final AuditService auditService;
 
     /**
      * Read broker config from the live broker via admin API.
      */
     public ClusterConfigVO getBrokerConfig(String brokerAddr) {
-        return adminFactory.execute(namesrvAddr(), null, admin -> {
+        return getBrokerConfig(brokerAddr, null);
+    }
+
+    public ClusterConfigVO getBrokerConfig(String brokerAddr, String 
instanceId) {
+        return execute(instanceId, admin -> {
             try {
                 Properties props = admin.getBrokerConfig(brokerAddr);
                 return mapToClusterConfigVO(props);
@@ -52,11 +58,22 @@ public class RocketMQBrokerConfigService {
         });
     }
 
+    private <T> T execute(String instanceId, MqAdminExtFactory.AdminAction<T> 
action) {
+        if (StringUtils.hasText(instanceId)) {
+            return runtimeAdminClientResolver.execute(instanceId, action);
+        }
+        return adminFactory.execute(namesrvAddr(), null, action);
+    }
+
     /**
      * Update broker config on the live broker via admin API, then record 
audit.
      */
     public void updateBrokerConfig(String brokerAddr, String clusterId, 
Properties newConfig) {
-        adminFactory.execute(namesrvAddr(), null, admin -> {
+        updateBrokerConfig(brokerAddr, clusterId, null, newConfig);
+    }
+
+    public void updateBrokerConfig(String brokerAddr, String clusterId, String 
instanceId, Properties newConfig) {
+        execute(instanceId, admin -> {
             try {
                 admin.updateBrokerConfig(brokerAddr, newConfig);
                 String detail = "brokerAddr=" + brokerAddr + ", config=" + 
newConfig;
@@ -74,7 +91,7 @@ public class RocketMQBrokerConfigService {
 
     private void recordAudit(String clusterId, String detail, String result) {
         try {
-            auditService.record("UPDATE_BROKER_CONFIG", "CLUSTER:" + 
clusterId, detail, result);
+            auditService.record("UPDATE_BROKER_CONFIG", "CLUSTER:" + 
clusterId, clusterId, detail, result);
         } catch (Exception auditFailure) {
             log.warn("Failed to record broker config audit for cluster {}: 
{}", clusterId,
                     auditFailure.getMessage());
diff --git 
a/server/src/main/java/org/apache/rocketmq/studio/provider/apache/RocketMQClusterProvider.java
 
b/server/src/main/java/org/apache/rocketmq/studio/provider/apache/RocketMQClusterProvider.java
index a5615753..51d333d5 100644
--- 
a/server/src/main/java/org/apache/rocketmq/studio/provider/apache/RocketMQClusterProvider.java
+++ 
b/server/src/main/java/org/apache/rocketmq/studio/provider/apache/RocketMQClusterProvider.java
@@ -23,6 +23,7 @@ import org.apache.rocketmq.studio.cluster.broker.BrokerVO;
 import org.apache.rocketmq.studio.cluster.broker.ClusterProvider;
 import org.apache.rocketmq.studio.cluster.broker.ClusterVO;
 import org.apache.rocketmq.studio.cluster.broker.MqAdminExtFactory;
+import org.apache.rocketmq.studio.cluster.broker.RuntimeAdminClientResolver;
 import org.apache.rocketmq.studio.cluster.nameserver.NameServerVO;
 import org.apache.rocketmq.studio.common.exception.BusinessException;
 import org.apache.rocketmq.studio.common.domain.enums.BrokerStatus;
@@ -52,10 +53,16 @@ public class RocketMQClusterProvider implements 
ClusterProvider {
 
     private final MqAdminExtFactory adminFactory;
     private final RocketMQProperties properties;
+    private final RuntimeAdminClientResolver runtimeAdminClientResolver;
 
     @Override
     public List<ClusterVO> discoverClusters() {
-        String namesrvAddr = properties.getNamesrvAddr();
+        return discoverClusters(null);
+    }
+
+    @Override
+    public List<ClusterVO> discoverClusters(String instanceId) {
+        String namesrvAddr = resolveNamesrvAddr(instanceId);
         if (!StringUtils.hasText(namesrvAddr)) {
             log.debug("NameServer address not configured, returning empty 
cluster list");
             return Collections.emptyList();
@@ -77,7 +84,7 @@ public class RocketMQClusterProvider implements 
ClusterProvider {
                     Set<String> brokerNames = entry.getValue();
 
                     List<BrokerVO> brokers = buildBrokerList(admin, 
brokerNames, brokerAddrTable);
-                    List<NameServerVO> nameServers = buildNameServerList();
+                    List<NameServerVO> nameServers = 
buildNameServerList(namesrvAddr);
 
                     ClusterVO cluster = buildClusterVO(clusterName, brokers, 
nameServers);
                     clusters.add(cluster);
@@ -93,7 +100,12 @@ public class RocketMQClusterProvider implements 
ClusterProvider {
 
     @Override
     public ClusterVO refreshClusterDetail(String clusterId) {
-        String namesrvAddr = properties.getNamesrvAddr();
+        return refreshClusterDetail(clusterId, null);
+    }
+
+    @Override
+    public ClusterVO refreshClusterDetail(String clusterId, String instanceId) 
{
+        String namesrvAddr = resolveNamesrvAddr(instanceId);
         if (!StringUtils.hasText(namesrvAddr)) {
             log.debug("NameServer address not configured, cannot refresh 
cluster detail");
             return null;
@@ -115,7 +127,7 @@ public class RocketMQClusterProvider implements 
ClusterProvider {
                 }
 
                 List<BrokerVO> brokers = buildBrokerList(admin, brokerNames, 
brokerAddrTable);
-                List<NameServerVO> nameServers = buildNameServerList();
+                List<NameServerVO> nameServers = 
buildNameServerList(namesrvAddr);
 
                 return buildClusterVO(clusterId, brokers, nameServers);
             });
@@ -253,9 +265,15 @@ public class RocketMQClusterProvider implements 
ClusterProvider {
         return 0;
     }
 
-    private List<NameServerVO> buildNameServerList() {
+    private String resolveNamesrvAddr(String instanceId) {
+        if (StringUtils.hasText(instanceId)) {
+            return runtimeAdminClientResolver.resolveEndpoint(instanceId);
+        }
+        return properties.getNamesrvAddr();
+    }
+
+    private List<NameServerVO> buildNameServerList(String namesrvAddr) {
         List<NameServerVO> nameServers = new ArrayList<>();
-        String namesrvAddr = properties.getNamesrvAddr();
         if (namesrvAddr == null || namesrvAddr.isEmpty()) {
             return nameServers;
         }
diff --git 
a/server/src/test/java/org/apache/rocketmq/studio/cluster/broker/ClusterControllerTest.java
 
b/server/src/test/java/org/apache/rocketmq/studio/cluster/broker/ClusterControllerTest.java
index 2bd3b758..c5d0d6cd 100644
--- 
a/server/src/test/java/org/apache/rocketmq/studio/cluster/broker/ClusterControllerTest.java
+++ 
b/server/src/test/java/org/apache/rocketmq/studio/cluster/broker/ClusterControllerTest.java
@@ -41,6 +41,7 @@ import java.util.Collections;
 import java.util.stream.Stream;
 
 import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.ArgumentMatchers.isNull;
 import static org.mockito.Mockito.verify;
 import static org.mockito.Mockito.verifyNoInteractions;
 import static org.mockito.Mockito.when;
@@ -111,7 +112,7 @@ class ClusterControllerTest {
     void listClustersShouldReturnAllClusters() throws Exception {
         ClusterVO cluster1 = buildCluster("cluster-1", "production-cluster", 
ClusterStatus.healthy);
         ClusterVO cluster2 = buildCluster("cluster-2", "staging-cluster", 
ClusterStatus.warning);
-        when(clusterService.listClusters()).thenReturn(Arrays.asList(cluster1, 
cluster2));
+        
when(clusterService.listClusters(isNull())).thenReturn(Arrays.asList(cluster1, 
cluster2));
 
         mockMvc.perform(get("/api/clusters"))
                 .andExpect(status().isOk())
@@ -128,7 +129,7 @@ class ClusterControllerTest {
 
     @Test
     void listClustersShouldReturnEmptyArrayWhenNoClusters() throws Exception {
-        
when(clusterService.listClusters()).thenReturn(Collections.emptyList());
+        
when(clusterService.listClusters(isNull())).thenReturn(Collections.emptyList());
 
         mockMvc.perform(get("/api/clusters"))
                 .andExpect(status().isOk())
@@ -147,7 +148,7 @@ class ClusterControllerTest {
                 .maxMessageSize(4194304)
                 .autoCreateTopicEnable(true)
                 .build());
-        when(clusterService.getCluster("cluster-1")).thenReturn(cluster);
+        when(clusterService.getCluster("cluster-1", null)).thenReturn(cluster);
 
         mockMvc.perform(get("/api/clusters/cluster-1"))
                 .andExpect(status().isOk())
diff --git 
a/server/src/test/java/org/apache/rocketmq/studio/cluster/broker/ClusterServiceTest.java
 
b/server/src/test/java/org/apache/rocketmq/studio/cluster/broker/ClusterServiceTest.java
index ca9f193f..f5fba221 100644
--- 
a/server/src/test/java/org/apache/rocketmq/studio/cluster/broker/ClusterServiceTest.java
+++ 
b/server/src/test/java/org/apache/rocketmq/studio/cluster/broker/ClusterServiceTest.java
@@ -132,6 +132,17 @@ class ClusterServiceTest {
         verify(clusterRepository, never()).findAll();
     }
 
+    @Test
+    void listClustersShouldUseSelectedInstance() {
+        
when(clusterProvider.discoverClusters("instance-1")).thenReturn(List.of(sampleCluster));
+
+        List<ClusterVO> result = clusterService.listClusters("instance-1");
+
+        assertThat(result).containsExactly(sampleCluster);
+        verify(clusterProvider).discoverClusters("instance-1");
+        verify(clusterProvider, never()).discoverClusters();
+    }
+
     @Test
     void 
updateClusterConfigShouldRejectDifferentDefaultReadAndWriteQueueNums() {
         UpdateConfigDTO command = UpdateConfigDTO.builder()
@@ -169,6 +180,17 @@ class ClusterServiceTest {
         assertThat(result.getType()).isEqualTo(ClusterType.V5_PROXY_CLUSTER);
     }
 
+    @Test
+    void getClusterShouldUseSelectedInstance() {
+        when(clusterProvider.refreshClusterDetail("cluster-1", 
"instance-1")).thenReturn(sampleCluster);
+
+        ClusterVO result = clusterService.getCluster("cluster-1", 
"instance-1");
+
+        assertThat(result).isSameAs(sampleCluster);
+        verify(clusterProvider).refreshClusterDetail("cluster-1", 
"instance-1");
+        verify(clusterProvider, never()).refreshClusterDetail("cluster-1");
+    }
+
     @Test
     void getClusterShouldThrowWhenNotFound() {
         
when(clusterProvider.refreshClusterDetail("nonexistent")).thenReturn(null);
@@ -198,7 +220,7 @@ class ClusterServiceTest {
     void updateConfigShouldSucceedWhenAuditRecordingFails() {
         
when(clusterRepository.findById("cluster-1")).thenReturn(Optional.of(sampleCluster));
         doThrow(new IllegalStateException("audit storage 
unavailable")).when(auditService)
-                .record(any(), any(), any(), any());
+                .record(any(), any(), any(), any(), any());
 
         ClusterConfigUpdateResultVO result = 
clusterService.updateClusterConfig(UpdateConfigDTO.builder()
                 .id("cluster-1")
@@ -294,10 +316,28 @@ class ClusterServiceTest {
         verify(auditService).record(
                 eq("UPDATE_CLUSTER_CONFIG"),
                 eq("CLUSTER:cluster-1"),
+                eq("cluster-1"),
                 org.mockito.ArgumentMatchers.contains("10.0.0.2:10911"),
                 eq("PARTIAL"));
     }
 
+    @Test
+    void updateConfigShouldUseSelectedInstanceForBrokerUpdates() {
+        
sampleCluster.setBrokers(List.of(BrokerVO.builder().name("broker-0").addr("10.0.0.1:10911").build()));
+        when(clusterProvider.refreshClusterDetail("cluster-1", 
"instance-1")).thenReturn(sampleCluster);
+
+        ClusterConfigUpdateResultVO result = 
clusterService.updateClusterConfig(UpdateConfigDTO.builder()
+                .id("cluster-1")
+                .instanceId("instance-1")
+                .writeQueueNums(16)
+                .build());
+
+        
assertThat(result.getStatus()).isEqualTo(ClusterConfigUpdateResultVO.Status.SUCCESS);
+        verify(clusterProvider).refreshClusterDetail("cluster-1", 
"instance-1");
+        verify(brokerConfigService).updateBrokerConfig(
+                eq("10.0.0.1:10911"), eq("cluster-1"), eq("instance-1"), 
any());
+    }
+
     @Test
     void updateConfigShouldThrowWhenClusterNotFound() {
         
when(clusterRepository.findById("missing")).thenReturn(Optional.empty());
diff --git 
a/server/src/test/java/org/apache/rocketmq/studio/ops/audit/AuditServiceTest.java
 
b/server/src/test/java/org/apache/rocketmq/studio/ops/audit/AuditServiceTest.java
index 6ac8ace3..43593b19 100644
--- 
a/server/src/test/java/org/apache/rocketmq/studio/ops/audit/AuditServiceTest.java
+++ 
b/server/src/test/java/org/apache/rocketmq/studio/ops/audit/AuditServiceTest.java
@@ -63,6 +63,16 @@ class AuditServiceTest {
         assertThat(captor.getValue().getOperator()).isEqualTo("operator-user");
     }
 
+    @Test
+    void recordShouldPreserveClusterIdWhenProvided() {
+        auditService.record("UPDATE_CLUSTER_CONFIG", "CLUSTER:prod-cn", 
"prod-cn",
+                "updated broker config", "SUCCESS");
+
+        ArgumentCaptor<AuditRecordVO> captor = 
ArgumentCaptor.forClass(AuditRecordVO.class);
+        verify(auditRepository).save(captor.capture());
+        assertThat(captor.getValue().getClusterId()).isEqualTo("prod-cn");
+    }
+
     @Test
     void queryLogsDelegatesPaginationAndFiltersToRepository() {
         AuditRecordVO record = 
AuditRecordVO.builder().operationType("CREATE").build();
diff --git 
a/server/src/test/java/org/apache/rocketmq/studio/provider/apache/RocketMQBrokerConfigServiceTest.java
 
b/server/src/test/java/org/apache/rocketmq/studio/provider/apache/RocketMQBrokerConfigServiceTest.java
index 83b8fe4d..0c85b1ba 100644
--- 
a/server/src/test/java/org/apache/rocketmq/studio/provider/apache/RocketMQBrokerConfigServiceTest.java
+++ 
b/server/src/test/java/org/apache/rocketmq/studio/provider/apache/RocketMQBrokerConfigServiceTest.java
@@ -6,6 +6,7 @@
  */
 package org.apache.rocketmq.studio.provider.apache;
 
+import org.apache.rocketmq.studio.cluster.broker.RuntimeAdminClientResolver;
 import org.apache.rocketmq.studio.cluster.broker.MqAdminExtFactory;
 import org.apache.rocketmq.studio.common.exception.BusinessException;
 import org.apache.rocketmq.studio.ops.audit.AuditService;
@@ -24,6 +25,7 @@ import static org.mockito.ArgumentMatchers.anyString;
 import static org.mockito.Mockito.doNothing;
 import static org.mockito.Mockito.doThrow;
 import static org.mockito.Mockito.lenient;
+import static org.mockito.Mockito.verify;
 
 @ExtendWith(MockitoExtension.class)
 class RocketMQBrokerConfigServiceTest {
@@ -36,6 +38,8 @@ class RocketMQBrokerConfigServiceTest {
     private DefaultMQAdminExt adminExt;
     @Mock
     private AuditService auditService;
+    @Mock
+    private RuntimeAdminClientResolver runtimeAdminClientResolver;
 
     private RocketMQBrokerConfigService brokerConfigService;
 
@@ -44,7 +48,8 @@ class RocketMQBrokerConfigServiceTest {
         
lenient().when(properties.getNamesrvAddr()).thenReturn("10.0.0.1:9876");
         lenient().when(adminFactory.execute(anyString(), any(), 
any())).thenAnswer(invocation ->
                 
invocation.<MqAdminExtFactory.AdminAction<Object>>getArgument(2).apply(adminExt));
-        brokerConfigService = new RocketMQBrokerConfigService(adminFactory, 
properties, auditService);
+        brokerConfigService = new RocketMQBrokerConfigService(
+                adminFactory, properties, runtimeAdminClientResolver, 
auditService);
     }
 
     @Test
@@ -53,7 +58,7 @@ class RocketMQBrokerConfigServiceTest {
         config.setProperty("flushDiskType", "ASYNC_FLUSH");
         doNothing().when(adminExt).updateBrokerConfig("broker-a:10911", 
config);
         doThrow(new IllegalStateException("audit db down")).when(auditService)
-                .record(anyString(), anyString(), anyString(), anyString());
+                .record(anyString(), anyString(), anyString(), anyString(), 
anyString());
 
         brokerConfigService.updateBrokerConfig("broker-a:10911", "cluster-a", 
config);
     }
@@ -64,10 +69,22 @@ class RocketMQBrokerConfigServiceTest {
         doThrow(new IllegalStateException("broker unavailable")).when(adminExt)
                 .updateBrokerConfig("broker-a:10911", config);
         doThrow(new IllegalStateException("audit db down")).when(auditService)
-                .record(anyString(), anyString(), anyString(), anyString());
+                .record(anyString(), anyString(), anyString(), anyString(), 
anyString());
 
         assertThatThrownBy(() -> 
brokerConfigService.updateBrokerConfig("broker-a:10911", "cluster-a", config))
                 .isInstanceOf(BusinessException.class)
                 .hasMessage("Failed to update broker config: broker 
unavailable");
     }
+
+    @Test
+    void updateRecordsStructuredClusterId() throws Exception {
+        Properties config = new Properties();
+        doNothing().when(adminExt).updateBrokerConfig("broker-a:10911", 
config);
+
+        brokerConfigService.updateBrokerConfig("broker-a:10911", "cluster-a", 
config);
+
+        verify(auditService).record(
+                "UPDATE_BROKER_CONFIG", "CLUSTER:cluster-a", "cluster-a",
+                "brokerAddr=broker-a:10911, config={}", "SUCCESS");
+    }
 }
diff --git 
a/server/src/test/java/org/apache/rocketmq/studio/provider/apache/RocketMQClusterProviderTest.java
 
b/server/src/test/java/org/apache/rocketmq/studio/provider/apache/RocketMQClusterProviderTest.java
index bf9d20ec..7cbf83b2 100644
--- 
a/server/src/test/java/org/apache/rocketmq/studio/provider/apache/RocketMQClusterProviderTest.java
+++ 
b/server/src/test/java/org/apache/rocketmq/studio/provider/apache/RocketMQClusterProviderTest.java
@@ -21,6 +21,7 @@ import org.apache.rocketmq.remoting.protocol.body.KVTable;
 import org.apache.rocketmq.remoting.protocol.route.BrokerData;
 import org.apache.rocketmq.studio.cluster.broker.ClusterVO;
 import org.apache.rocketmq.studio.cluster.broker.MqAdminExtFactory;
+import org.apache.rocketmq.studio.cluster.broker.RuntimeAdminClientResolver;
 import org.apache.rocketmq.studio.common.domain.enums.ClusterStatus;
 import org.apache.rocketmq.studio.common.exception.BusinessException;
 import org.apache.rocketmq.tools.admin.DefaultMQAdminExt;
@@ -113,7 +114,7 @@ class RocketMQClusterProviderTest {
                 
invocation.<MqAdminExtFactory.AdminAction<Object>>getArgument(2).apply(adminExt));
         RocketMQProperties properties = new RocketMQProperties();
         properties.setNamesrvAddr("10.0.0.1:9876");
-        return new RocketMQClusterProvider(adminFactory, properties);
+        return new RocketMQClusterProvider(adminFactory, properties, 
mock(RuntimeAdminClientResolver.class));
     }
 
     @Test
diff --git a/web/src/api/cluster.ts b/web/src/api/cluster.ts
index 0de78ce7..b7851da9 100644
--- a/web/src/api/cluster.ts
+++ b/web/src/api/cluster.ts
@@ -135,8 +135,10 @@ export interface NameServerConfigDiffResult {
 }
 
 // ─── Cluster ────────────────────────────────────────────────────
-export async function listClusters() {
-  const res = await client.get<{ data: ClusterInfo[] }>('/clusters');
+export async function listClusters(instanceId?: string) {
+  const res = await client.get<{ data: ClusterInfo[] }>('/clusters', {
+    params: instanceId ? { instanceId } : undefined,
+  });
   return res.data.data;
 }
 
@@ -147,12 +149,16 @@ export async function testClusterConnection(namesrvAddr: 
string) {
   return res.data.data;
 }
 
-export async function getCluster(id: string) {
-  const res = await client.get<{ data: ClusterInfo 
}>(`/clusters/${pathSegment(id)}`);
+export async function getCluster(id: string, instanceId?: string) {
+  const res = await client.get<{ data: ClusterInfo 
}>(`/clusters/${pathSegment(id)}`, {
+    params: instanceId ? { instanceId } : undefined,
+  });
   return res.data.data;
 }
 
-export async function updateClusterConfig(data: { id: string } & 
Partial<ClusterConfig>) {
+export async function updateClusterConfig(
+  data: { id: string; instanceId?: string } & Partial<ClusterConfig>,
+) {
   const res = await client.post<{ data: ClusterConfigUpdateResult }>(
     '/clusters/config/update',
     data,
diff --git a/web/src/pages/cluster/index.tsx b/web/src/pages/cluster/index.tsx
index 0db8c857..c4088809 100644
--- a/web/src/pages/cluster/index.tsx
+++ b/web/src/pages/cluster/index.tsx
@@ -64,6 +64,8 @@ import {
   updateClusterConfig,
   updateNameServer,
 } from '../../services/clusterService';
+import { listInstances } from '../../services/instanceService';
+import type { Instance } from '../../api/instance';
 
 const { Text } = Typography;
 
@@ -83,6 +85,8 @@ const compareText = (left: string | null | undefined, right: 
string | null | und
 const ClusterPage = () => {
   const { t } = useLang();
   const [clusters, setClusters] = useState<ClusterInfo[]>([]);
+  const [instances, setInstances] = useState<Instance[]>([]);
+  const [selectedInstanceId, setSelectedInstanceId] = useState('');
   const [loading, setLoading] = useState(true);
   const [nsSearch, setNsSearch] = useState('');
   const [brokerSearch, setBrokerSearch] = useState('');
@@ -148,6 +152,18 @@ const ClusterPage = () => {
     Promise.resolve(),
   );
   const tRef = useRef(t);
+  const selectedInstanceIdRef = useRef('');
+
+  useEffect(() => {
+    void listInstances().then((nextInstances) => {
+      const apacheInstances = nextInstances.filter((instance) => 
instance.vendor === 'APACHE');
+      setInstances(apacheInstances);
+      const initialInstanceId = apacheInstances[0]?.id ?? '';
+      selectedInstanceIdRef.current = initialInstanceId;
+      setSelectedInstanceId(initialInstanceId);
+      void requestRefreshRef.current('manual');
+    });
+  }, []);
 
   const clearRefreshTimer = useCallback(() => {
     if (refreshTimerRef.current !== null) {
@@ -177,7 +193,7 @@ const ClusterPage = () => {
 
         while (currentSource && mountedRef.current) {
           try {
-            const nextClusters = await listClusters();
+            const nextClusters = await 
listClusters(selectedInstanceIdRef.current || undefined);
             if (!mountedRef.current) return;
             setClusters(nextClusters);
             setSelectedProxy((current) => {
@@ -530,6 +546,7 @@ const ClusterPage = () => {
                   };
                   const result = await updateClusterConfig({
                     id: selectedCluster.id,
+                    instanceId: selectedInstanceIdRef.current || undefined,
                     ...nextConfig,
                   });
                   if (result.status === 'SUCCESS') {
@@ -730,6 +747,17 @@ const ClusterPage = () => {
       <div>
         <Flex justify="space-between" style={{ marginBottom: 16 }}>
           <Space>
+            <Select
+              value={selectedInstanceId || undefined}
+              onChange={(instanceId) => {
+                selectedInstanceIdRef.current = instanceId;
+                setSelectedInstanceId(instanceId);
+                void requestRefresh('manual');
+              }}
+              placeholder="Select instance"
+              style={{ width: 180 }}
+              options={instances.map((instance) => ({ value: instance.id, 
label: instance.name }))}
+            />
             <Input.Search
               placeholder={t('cluster.searchNs')}
               allowClear
diff --git a/web/src/services/clusterService.ts 
b/web/src/services/clusterService.ts
index b15d9760..65bef100 100644
--- a/web/src/services/clusterService.ts
+++ b/web/src/services/clusterService.ts
@@ -34,11 +34,11 @@ function copyCluster(cluster: ClusterInfo): ClusterInfo {
   };
 }
 
-export async function listClusters(): Promise<ClusterInfo[]> {
+export async function listClusters(instanceId?: string): 
Promise<ClusterInfo[]> {
   if (isMockMode()) {
     return clusters.map(copyCluster);
   }
-  return clusterApi.listClusters();
+  return clusterApi.listClusters(instanceId);
 }
 
 export async function testClusterConnection(namesrvAddr: string): 
Promise<ClusterProbeResult> {
@@ -59,13 +59,13 @@ export async function testClusterConnection(namesrvAddr: 
string): Promise<Cluste
   return clusterApi.testClusterConnection(namesrvAddr);
 }
 
-export async function getCluster(id: string): Promise<ClusterInfo> {
+export async function getCluster(id: string, instanceId?: string): 
Promise<ClusterInfo> {
   if (isMockMode()) {
     const cluster = clusters.find((item) => item.id === id);
     if (!cluster) throw new Error('Cluster not found');
     return copyCluster(cluster);
   }
-  return clusterApi.getCluster(id);
+  return clusterApi.getCluster(id, instanceId);
 }
 
 export async function getNameServerConfigDiff(
@@ -171,7 +171,9 @@ export async function deleteK8sCert(id: string): 
Promise<void> {
   return clusterApi.deleteK8sCert(id);
 }
 
-export async function updateClusterConfig(data: { id: string } & 
Partial<ClusterConfig>) {
+export async function updateClusterConfig(
+  data: { id: string; instanceId?: string } & Partial<ClusterConfig>,
+) {
   if (isMockMode()) {
     const { id, ...config } = data;
     const cluster = getMockCluster(id);

Reply via email to