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 });
+ }
},
}));