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 03d8aadc fix: harden cluster repository and config state (#645)
03d8aadc is described below

commit 03d8aadcb24e720b499f4fd678990687663dbd27
Author: aias00 <[email protected]>
AuthorDate: Tue Jul 28 07:33:50 2026 -0700

    fix: harden cluster repository and config state (#645)
    
    * [Studio] Stabilize cluster identity and ordering
    
    * [Studio] Harden cluster config and store loading
---
 .../studio/cluster/broker/ClusterProviderStub.java | 10 +--
 .../cluster/broker/ClusterRepositoryImpl.java      |  8 ++-
 .../studio/cluster/broker/ClusterService.java      | 10 ++-
 .../cluster/broker/ClusterProviderStubTest.java    | 48 +++++++++++++
 .../cluster/broker/ClusterRepositoryImplTest.java  | 33 ++++-----
 .../studio/cluster/broker/ClusterServiceTest.java  | 17 +++++
 web/src/stores/clusterStore.test.ts                | 84 ++++++++++++++++++++++
 web/src/stores/clusterStore.ts                     | 15 ++--
 8 files changed, 194 insertions(+), 31 deletions(-)

diff --git 
a/server/src/main/java/org/apache/rocketmq/studio/cluster/broker/ClusterProviderStub.java
 
b/server/src/main/java/org/apache/rocketmq/studio/cluster/broker/ClusterProviderStub.java
index 12ed2980..10e7271e 100644
--- 
a/server/src/main/java/org/apache/rocketmq/studio/cluster/broker/ClusterProviderStub.java
+++ 
b/server/src/main/java/org/apache/rocketmq/studio/cluster/broker/ClusterProviderStub.java
@@ -30,16 +30,16 @@ public class ClusterProviderStub implements ClusterProvider 
{
 
     @Override
     public List<ClusterVO> discoverClusters() {
-        return List.of(buildSampleCluster());
+        return List.of(buildSampleCluster("cluster-001"));
     }
 
     @Override
     public ClusterVO refreshClusterDetail(String clusterId) {
-        return buildSampleCluster();
+        return buildSampleCluster(clusterId);
     }
 
-    private ClusterVO buildSampleCluster() {
-        return ClusterVO.builder()
+    private ClusterVO buildSampleCluster(String clusterId) {
+        ClusterVO cluster = ClusterVO.builder()
                 .name("rmq-cluster-01")
                 .brokers(List.of(
                         BrokerVO.builder()
@@ -81,5 +81,7 @@ public class ClusterProviderStub implements ClusterProvider {
                                 .build()
                 ))
                 .build();
+        cluster.setId(clusterId);
+        return cluster;
     }
 }
diff --git 
a/server/src/main/java/org/apache/rocketmq/studio/cluster/broker/ClusterRepositoryImpl.java
 
b/server/src/main/java/org/apache/rocketmq/studio/cluster/broker/ClusterRepositoryImpl.java
index 346c3f9e..7bdfa8e0 100644
--- 
a/server/src/main/java/org/apache/rocketmq/studio/cluster/broker/ClusterRepositoryImpl.java
+++ 
b/server/src/main/java/org/apache/rocketmq/studio/cluster/broker/ClusterRepositoryImpl.java
@@ -28,7 +28,7 @@ import lombok.extern.slf4j.Slf4j;
 import org.springframework.stereotype.Repository;
 
 import java.time.LocalDateTime;
-import java.util.ArrayList;
+import java.util.Comparator;
 import java.util.List;
 import java.util.Map;
 import java.util.Optional;
@@ -46,7 +46,11 @@ public class ClusterRepositoryImpl implements 
ClusterRepository {
 
     @Override
     public List<ClusterVO> findAll() {
-        return new ArrayList<>(store.values());
+        return store.values().stream()
+                .sorted(Comparator
+                        .comparing(ClusterVO::getName, 
Comparator.nullsLast(String::compareToIgnoreCase))
+                        .thenComparing(ClusterVO::getId, 
Comparator.nullsLast(String::compareTo)))
+                .toList();
     }
 
     @Override
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 7aa216f2..e32bf14f 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
@@ -65,7 +65,7 @@ public class ClusterService {
         }
 
         if (command.getFlushDiskType() != null) {
-            
config.setFlushDiskType(FlushDiskType.valueOf(command.getFlushDiskType()));
+            
config.setFlushDiskType(parseFlushDiskType(command.getFlushDiskType()));
         }
         if (command.getAutoCreateTopicEnable() != null) {
             
config.setAutoCreateTopicEnable(command.getAutoCreateTopicEnable());
@@ -95,6 +95,14 @@ public class ClusterService {
         return cluster;
     }
 
+    private FlushDiskType parseFlushDiskType(String value) {
+        try {
+            return FlushDiskType.valueOf(value);
+        } catch (IllegalArgumentException ex) {
+            throw new BusinessException(400, "Invalid flushDiskType: " + 
value);
+        }
+    }
+
     public boolean restartBroker(String clusterId, String brokerName) {
         log.info("Restarting broker: {} in cluster: {}", brokerName, 
clusterId);
         clusterRepository.findById(clusterId)
diff --git 
a/server/src/test/java/org/apache/rocketmq/studio/cluster/broker/ClusterProviderStubTest.java
 
b/server/src/test/java/org/apache/rocketmq/studio/cluster/broker/ClusterProviderStubTest.java
new file mode 100644
index 00000000..a9882330
--- /dev/null
+++ 
b/server/src/test/java/org/apache/rocketmq/studio/cluster/broker/ClusterProviderStubTest.java
@@ -0,0 +1,48 @@
+/*
+ * 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.rocketmq.studio.cluster.broker;
+
+import org.junit.jupiter.api.Test;
+
+import java.util.List;
+
+import static org.assertj.core.api.Assertions.assertThat;
+
+class ClusterProviderStubTest {
+
+    private final ClusterProviderStub provider = new ClusterProviderStub();
+
+    @Test
+    void discoverClustersShouldReturnStableClusterId() {
+        List<ClusterVO> clusters = provider.discoverClusters();
+
+        assertThat(clusters).hasSize(1);
+        assertThat(clusters.get(0).getId()).isEqualTo("cluster-001");
+        assertThat(clusters.get(0).getBrokers()).isNotEmpty();
+        assertThat(clusters.get(0).getProxies()).isNotEmpty();
+        assertThat(clusters.get(0).getNameServers()).isNotEmpty();
+    }
+
+    @Test
+    void refreshClusterDetailShouldPreserveRequestedClusterId() {
+        ClusterVO detail = provider.refreshClusterDetail("cluster-prod");
+
+        assertThat(detail.getId()).isEqualTo("cluster-prod");
+        assertThat(detail.getName()).isEqualTo("rmq-cluster-01");
+        assertThat(detail.getBrokers()).hasSize(2);
+    }
+}
diff --git a/web/src/stores/clusterStore.ts 
b/server/src/test/java/org/apache/rocketmq/studio/cluster/broker/ClusterRepositoryImplTest.java
similarity index 59%
copy from web/src/stores/clusterStore.ts
copy to 
server/src/test/java/org/apache/rocketmq/studio/cluster/broker/ClusterRepositoryImplTest.java
index c2770046..e805803e 100644
--- a/web/src/stores/clusterStore.ts
+++ 
b/server/src/test/java/org/apache/rocketmq/studio/cluster/broker/ClusterRepositoryImplTest.java
@@ -14,26 +14,23 @@
  * See the License for the specific language governing permissions and
  * limitations under the License.
  */
+package org.apache.rocketmq.studio.cluster.broker;
 
-import { create } from 'zustand';
+import org.junit.jupiter.api.Test;
 
-// eslint-disable-next-line @typescript-eslint/no-explicit-any
-type Cluster = any;
+import java.util.List;
 
-interface ClusterState {
-  clusters: Cluster[];
-  loading: boolean;
-  fetchClusters: () => Promise<void>;
-}
+import static org.assertj.core.api.Assertions.assertThat;
+
+class ClusterRepositoryImplTest {
 
-const useClusterStore = create<ClusterState>((set) => ({
-  clusters: [],
-  loading: false,
-  fetchClusters: async () => {
-    set({ loading: true });
-    // TODO: call API to fetch clusters
-    set({ clusters: [], loading: false });
-  },
-}));
+    @Test
+    void findAllShouldReturnClustersInStableNameOrder() {
+        ClusterRepositoryImpl repository = new ClusterRepositoryImpl();
 
-export default useClusterStore;
+        List<ClusterVO> clusters = repository.findAll();
+
+        assertThat(clusters).extracting(ClusterVO::getName)
+                .containsExactly("rmq-cluster-prod", "rmq-cluster-staging");
+    }
+}
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 bf4fe0f1..bece2bb7 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
@@ -39,6 +39,7 @@ import static org.assertj.core.api.Assertions.assertThat;
 import static org.assertj.core.api.Assertions.assertThatThrownBy;
 import static org.mockito.ArgumentMatchers.any;
 import static org.mockito.ArgumentMatchers.eq;
+import static org.mockito.Mockito.never;
 import static org.mockito.Mockito.verify;
 import static org.mockito.Mockito.when;
 
@@ -210,6 +211,22 @@ class ClusterServiceTest {
                 .hasMessageContaining("Cluster not found: missing");
     }
 
+    @Test
+    void updateConfigShouldRejectInvalidFlushDiskType() {
+        
when(clusterRepository.findById("cluster-1")).thenReturn(Optional.of(sampleCluster));
+
+        UpdateConfigDTO command = UpdateConfigDTO.builder()
+                .id("cluster-1")
+                .flushDiskType("INVALID_FLUSH")
+                .build();
+
+        assertThatThrownBy(() -> clusterService.updateClusterConfig(command))
+                .isInstanceOf(BusinessException.class)
+                .hasMessageContaining("Invalid flushDiskType: INVALID_FLUSH")
+                .satisfies(ex -> assertThat(((BusinessException) 
ex).getCode()).isEqualTo(400));
+        verify(clusterRepository, never()).updateConfig(eq("cluster-1"), 
any(ClusterConfigVO.class));
+    }
+
     @Test
     void updateConfigShouldCreateConfigWhenNull() {
         ClusterVO clusterWithNullConfig = ClusterVO.builder()
diff --git a/web/src/stores/clusterStore.test.ts 
b/web/src/stores/clusterStore.test.ts
new file mode 100644
index 00000000..3084b22c
--- /dev/null
+++ b/web/src/stores/clusterStore.test.ts
@@ -0,0 +1,84 @@
+/*
+ * 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 { afterEach, describe, expect, it, vi } from 'vitest';
+import type { ClusterInfo } from '../api/cluster';
+import { listClusters } from '../services/clusterService';
+import useClusterStore from './clusterStore';
+
+vi.mock('../services/clusterService', () => ({
+  listClusters: vi.fn(),
+}));
+
+const cluster: ClusterInfo = {
+  id: 'cluster-prod',
+  name: 'rocketmq-prod',
+  nsClusterName: 'ns-prod',
+  type: 'V5_PROXY_CLUSTER',
+  endpoint: '10.101.2.1:9876',
+  status: 'healthy',
+  version: '5.2.0',
+  brokers: [],
+  proxies: [],
+  nameServers: [],
+  config: {
+    flushDiskType: 'SYNC_FLUSH',
+    autoCreateTopicEnable: false,
+    autoCreateSubscriptionGroup: false,
+    maxMessageSize: 4194304,
+    msgTraceTopicName: 'RMQ_SYS_TRACE_TOPIC4',
+    fileReservedTime: 72,
+    writeQueueNums: 16,
+    readQueueNums: 16,
+    brokerPermission: 6,
+    deleteWhen: '04',
+  },
+  topicCount: 256,
+  groupCount: 128,
+  tpsHistory: [100, 120],
+};
+
+describe('clusterStore', () => {
+  afterEach(() => {
+    vi.mocked(listClusters).mockReset();
+    useClusterStore.setState({ clusters: [], loading: false });
+  });
+
+  it('loads clusters from the cluster service', async () => {
+    vi.mocked(listClusters).mockResolvedValue([cluster]);
+
+    await useClusterStore.getState().fetchClusters();
+
+    expect(listClusters).toHaveBeenCalledTimes(1);
+    expect(useClusterStore.getState()).toMatchObject({
+      clusters: [cluster],
+      loading: false,
+    });
+  });
+
+  it('resets loading when loading clusters fails', async () => {
+    const error = new Error('failed to load clusters');
+    vi.mocked(listClusters).mockRejectedValue(error);
+
+    await 
expect(useClusterStore.getState().fetchClusters()).rejects.toThrow(error);
+
+    expect(useClusterStore.getState()).toMatchObject({
+      clusters: [],
+      loading: false,
+    });
+  });
+});
diff --git a/web/src/stores/clusterStore.ts b/web/src/stores/clusterStore.ts
index c2770046..48795048 100644
--- a/web/src/stores/clusterStore.ts
+++ b/web/src/stores/clusterStore.ts
@@ -16,12 +16,11 @@
  */
 
 import { create } from 'zustand';
-
-// eslint-disable-next-line @typescript-eslint/no-explicit-any
-type Cluster = any;
+import { listClusters } from '../services/clusterService';
+import type { ClusterInfo } from '../api/cluster';
 
 interface ClusterState {
-  clusters: Cluster[];
+  clusters: ClusterInfo[];
   loading: boolean;
   fetchClusters: () => Promise<void>;
 }
@@ -31,8 +30,12 @@ const useClusterStore = create<ClusterState>((set) => ({
   loading: false,
   fetchClusters: async () => {
     set({ loading: true });
-    // TODO: call API to fetch clusters
-    set({ clusters: [], loading: false });
+    try {
+      const clusters = await listClusters();
+      set({ clusters });
+    } finally {
+      set({ loading: false });
+    }
   },
 }));
 

Reply via email to