From 29a2cf25947448e4835664d0bc1dbba1a16b9ff5 Mon Sep 17 00:00:00 2001 From: liuhy Date: Sun, 26 Jul 2026 19:32:55 -0700 Subject: [PATCH 1/2] [Studio] Normalize service search filters --- .../studio/cluster/client/ClientService.java | 6 +- .../client/ProducerConnectionService.java | 8 +- .../studio/instance/InstanceService.java | 9 +- .../group/ConsumerDiagnosticsService.java | 15 ++-- .../instance/topic/MetadataService.java | 11 ++- .../ops/audit/InMemoryAuditRepository.java | 20 ++++- .../cluster/client/ClientServiceTest.java | 63 ++++++++++++++ .../client/ProducerConnectionServiceTest.java | 19 +++++ .../studio/instance/InstanceServiceTest.java | 26 ++++++ .../group/ConsumerDiagnosticsServiceTest.java | 18 ++++ .../instance/topic/MetadataServiceTest.java | 34 ++++++++ .../audit/InMemoryAuditRepositoryTest.java | 83 +++++++++++++++++++ 12 files changed, 294 insertions(+), 18 deletions(-) create mode 100644 server/src/test/java/org/apache/rocketmq/studio/cluster/client/ClientServiceTest.java create mode 100644 server/src/test/java/org/apache/rocketmq/studio/ops/audit/InMemoryAuditRepositoryTest.java diff --git a/server/src/main/java/org/apache/rocketmq/studio/cluster/client/ClientService.java b/server/src/main/java/org/apache/rocketmq/studio/cluster/client/ClientService.java index 60469a0d..7f39325a 100644 --- a/server/src/main/java/org/apache/rocketmq/studio/cluster/client/ClientService.java +++ b/server/src/main/java/org/apache/rocketmq/studio/cluster/client/ClientService.java @@ -31,6 +31,10 @@ public class ClientService { public List listConnections(String clusterId, String type) { log.info("Listing client connections, clusterId={}, type={}", clusterId, type); - return clientProvider.findConnections(clusterId, type); + return clientProvider.findConnections(normalizeFilter(clusterId), normalizeFilter(type)); + } + + private String normalizeFilter(String value) { + return value == null || value.isBlank() ? null : value.trim(); } } diff --git a/server/src/main/java/org/apache/rocketmq/studio/cluster/client/ProducerConnectionService.java b/server/src/main/java/org/apache/rocketmq/studio/cluster/client/ProducerConnectionService.java index 9709db00..bce19e91 100644 --- a/server/src/main/java/org/apache/rocketmq/studio/cluster/client/ProducerConnectionService.java +++ b/server/src/main/java/org/apache/rocketmq/studio/cluster/client/ProducerConnectionService.java @@ -32,8 +32,10 @@ public class ProducerConnectionService { public List listConnections(String topic, String producerGroup) { log.info("Listing producer connections, topic={}, producerGroup={}", topic, producerGroup); + String normalizedTopic = normalizeFilter(topic); + String normalizedProducerGroup = normalizeFilter(producerGroup); return clientService.listConnections(null, ClientType.Producer.name()).stream() - .filter(connection -> matchesFilter(connection, topic, producerGroup)) + .filter(connection -> matchesFilter(connection, normalizedTopic, normalizedProducerGroup)) .map(this::toProducerConnection) .toList(); } @@ -60,4 +62,8 @@ private ProducerConnectionVO toProducerConnection(ClientConnectionVO connection) private boolean hasText(String value) { return value != null && !value.trim().isEmpty(); } + + private String normalizeFilter(String value) { + return hasText(value) ? value.trim() : null; + } } diff --git a/server/src/main/java/org/apache/rocketmq/studio/instance/InstanceService.java b/server/src/main/java/org/apache/rocketmq/studio/instance/InstanceService.java index f9abb650..eb4c5aec 100644 --- a/server/src/main/java/org/apache/rocketmq/studio/instance/InstanceService.java +++ b/server/src/main/java/org/apache/rocketmq/studio/instance/InstanceService.java @@ -36,13 +36,14 @@ public class InstanceService { public List listInstances(InstanceType type, String search) { log.debug("Listing instances, type={}, search={}", type, search); + String normalizedSearch = search == null || search.isBlank() ? null : search.trim(); - if (type != null && search != null && !search.isBlank()) { - return instanceRepository.findByTypeAndSearch(type, search); + if (type != null && normalizedSearch != null) { + return instanceRepository.findByTypeAndSearch(type, normalizedSearch); } else if (type != null) { return instanceRepository.findByType(type); - } else if (search != null && !search.isBlank()) { - return instanceRepository.search(search); + } else if (normalizedSearch != null) { + return instanceRepository.search(normalizedSearch); } return instanceRepository.findAll(); } diff --git a/server/src/main/java/org/apache/rocketmq/studio/instance/group/ConsumerDiagnosticsService.java b/server/src/main/java/org/apache/rocketmq/studio/instance/group/ConsumerDiagnosticsService.java index 4c4def1f..87e8f1db 100644 --- a/server/src/main/java/org/apache/rocketmq/studio/instance/group/ConsumerDiagnosticsService.java +++ b/server/src/main/java/org/apache/rocketmq/studio/instance/group/ConsumerDiagnosticsService.java @@ -30,12 +30,15 @@ public class ConsumerDiagnosticsService { private final ConsumerDiagnosticsProvider diagnosticsProvider; public ConsumerStackTraceVO getConsumerStack(String groupName, String clientId) { - if (!StringUtils.hasText(groupName)) { - throw new BusinessException(HttpStatus.BAD_REQUEST.value(), "groupName is required"); - } - if (!StringUtils.hasText(clientId)) { - throw new BusinessException(HttpStatus.BAD_REQUEST.value(), "clientId is required"); + String normalizedGroupName = normalizeRequired(groupName, "groupName"); + String normalizedClientId = normalizeRequired(clientId, "clientId"); + return diagnosticsProvider.getConsumerStack(normalizedGroupName, normalizedClientId); + } + + private String normalizeRequired(String value, String fieldName) { + if (!StringUtils.hasText(value)) { + throw new BusinessException(HttpStatus.BAD_REQUEST.value(), fieldName + " is required"); } - return diagnosticsProvider.getConsumerStack(groupName, clientId); + return value.trim(); } } diff --git a/server/src/main/java/org/apache/rocketmq/studio/instance/topic/MetadataService.java b/server/src/main/java/org/apache/rocketmq/studio/instance/topic/MetadataService.java index 33f53642..f2c4e541 100644 --- a/server/src/main/java/org/apache/rocketmq/studio/instance/topic/MetadataService.java +++ b/server/src/main/java/org/apache/rocketmq/studio/instance/topic/MetadataService.java @@ -38,7 +38,10 @@ public class MetadataService { public List listTopics(String clusterId, String type, String search) { - return metadataProvider.listTopics(clusterId, type, search); + return metadataProvider.listTopics( + normalizeFilter(clusterId), + normalizeFilter(type), + normalizeFilter(search)); } @@ -75,7 +78,7 @@ public SendMessageVO sendMessage(SendMessageDTO request) { public List listConsumerGroups(String clusterId, String search) { - return metadataProvider.listConsumerGroups(clusterId, search); + return metadataProvider.listConsumerGroups(normalizeFilter(clusterId), normalizeFilter(search)); } @@ -114,4 +117,8 @@ public void resetOffset(String name, long timestamp, String topic) { public List listNamespaces() { throw new UnsupportedOperationException("Not implemented"); } + + private String normalizeFilter(String value) { + return value == null || value.isBlank() ? null : value.trim(); + } } diff --git a/server/src/main/java/org/apache/rocketmq/studio/ops/audit/InMemoryAuditRepository.java b/server/src/main/java/org/apache/rocketmq/studio/ops/audit/InMemoryAuditRepository.java index 37b430c9..02740375 100644 --- a/server/src/main/java/org/apache/rocketmq/studio/ops/audit/InMemoryAuditRepository.java +++ b/server/src/main/java/org/apache/rocketmq/studio/ops/audit/InMemoryAuditRepository.java @@ -35,11 +35,12 @@ public class InMemoryAuditRepository implements AuditRepository { public List findAll(String search, String operationType, LocalDateTime startDate, LocalDateTime endDate, String result) { + String normalizedSearch = normalize(search); return records.values().stream() - .filter(r -> search == null || search.isEmpty() - || r.getDetail().toLowerCase().contains(search.toLowerCase()) - || r.getOperator().toLowerCase().contains(search.toLowerCase()) - || r.getTarget().toLowerCase().contains(search.toLowerCase())) + .filter(r -> normalizedSearch == null + || containsIgnoreCase(r.getDetail(), normalizedSearch) + || containsIgnoreCase(r.getOperator(), normalizedSearch) + || containsIgnoreCase(r.getTarget(), normalizedSearch)) .filter(r -> operationType == null || operationType.isEmpty() || operationType.equals(r.getOperationType())) .filter(r -> startDate == null || r.getTimestamp() != null && !r.getTimestamp().isBefore(startDate)) @@ -54,6 +55,17 @@ public List findAll(String search, String operationType, .collect(Collectors.toList()); } + private String normalize(String value) { + if (value == null || value.isBlank()) { + return null; + } + return value.toLowerCase(); + } + + private boolean containsIgnoreCase(String value, String normalizedSearch) { + return value != null && value.toLowerCase().contains(normalizedSearch); + } + @Override public int deleteBefore(LocalDateTime cutoff) { List toRemove = records.values().stream() diff --git a/server/src/test/java/org/apache/rocketmq/studio/cluster/client/ClientServiceTest.java b/server/src/test/java/org/apache/rocketmq/studio/cluster/client/ClientServiceTest.java new file mode 100644 index 00000000..6825cbf1 --- /dev/null +++ b/server/src/test/java/org/apache/rocketmq/studio/cluster/client/ClientServiceTest.java @@ -0,0 +1,63 @@ +/* + * 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.client; + +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; + +import java.util.List; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +@ExtendWith(MockitoExtension.class) +class ClientServiceTest { + + @Mock + private ClientProvider clientProvider; + + @InjectMocks + private ClientService clientService; + + @Test + void listConnectionsShouldTrimFiltersBeforeQueryingProvider() { + ClientConnectionVO connection = ClientConnectionVO.builder() + .clientId("client-1") + .clusterName("production-cluster") + .build(); + when(clientProvider.findConnections("production-cluster", "Producer")).thenReturn(List.of(connection)); + + List result = clientService.listConnections(" production-cluster ", " Producer "); + + assertThat(result).containsExactly(connection); + verify(clientProvider).findConnections("production-cluster", "Producer"); + } + + @Test + void listConnectionsShouldTreatBlankFiltersAsUnspecified() { + when(clientProvider.findConnections(null, null)).thenReturn(List.of()); + + List result = clientService.listConnections(" ", "\t"); + + assertThat(result).isEmpty(); + verify(clientProvider).findConnections(null, null); + } +} diff --git a/server/src/test/java/org/apache/rocketmq/studio/cluster/client/ProducerConnectionServiceTest.java b/server/src/test/java/org/apache/rocketmq/studio/cluster/client/ProducerConnectionServiceTest.java index 5bce6134..489f43cc 100644 --- a/server/src/test/java/org/apache/rocketmq/studio/cluster/client/ProducerConnectionServiceTest.java +++ b/server/src/test/java/org/apache/rocketmq/studio/cluster/client/ProducerConnectionServiceTest.java @@ -91,6 +91,25 @@ void listConnectionsShouldFallbackToProducerGroupWhenTopicIsMissing() { assertThat(result.get(0).getClientId()).isEqualTo("producer-1"); } + @Test + void listConnectionsShouldTrimFilterValues() { + ClientConnectionVO producer = ClientConnectionVO.builder() + .clientId("producer-1") + .type(ClientType.Producer) + .groupOrTopic("order-topic") + .producerGroup("pg-order") + .address("10.0.0.1:38888") + .language(ClientLanguage.Java) + .version("5.1.0") + .build(); + when(clientService.listConnections(null, ClientType.Producer.name())).thenReturn(List.of(producer)); + + List result = producerConnectionService.listConnections(" order-topic ", " pg-order "); + + assertThat(result).hasSize(1); + assertThat(result.get(0).getClientId()).isEqualTo("producer-1"); + } + @Test void listConnectionsShouldRequireProducerGroupWhenBothFiltersAreProvided() { ClientConnectionVO producer = ClientConnectionVO.builder() diff --git a/server/src/test/java/org/apache/rocketmq/studio/instance/InstanceServiceTest.java b/server/src/test/java/org/apache/rocketmq/studio/instance/InstanceServiceTest.java index 9fab45f6..c59644fd 100644 --- a/server/src/test/java/org/apache/rocketmq/studio/instance/InstanceServiceTest.java +++ b/server/src/test/java/org/apache/rocketmq/studio/instance/InstanceServiceTest.java @@ -83,6 +83,19 @@ void listInstancesShouldSearchByKeyword() { verify(instanceRepository).search("prod"); } + @Test + void listInstancesShouldTrimSearchKeyword() { + List instances = List.of( + InstanceVO.builder().name("production").build() + ); + when(instanceRepository.search("prod")).thenReturn(instances); + + List result = instanceService.listInstances(null, " prod "); + + assertThat(result).hasSize(1); + verify(instanceRepository).search("prod"); + } + @Test void listInstancesShouldFilterByTypeAndSearch() { List instances = List.of( @@ -96,6 +109,19 @@ void listInstancesShouldFilterByTypeAndSearch() { verify(instanceRepository).findByTypeAndSearch(InstanceType.PROXY, "prod"); } + @Test + void listInstancesShouldTrimSearchKeywordWhenFilteringByType() { + List instances = List.of( + InstanceVO.builder().name("production-proxy").type(InstanceType.PROXY).build() + ); + when(instanceRepository.findByTypeAndSearch(InstanceType.PROXY, "prod")).thenReturn(instances); + + List result = instanceService.listInstances(InstanceType.PROXY, " prod "); + + assertThat(result).hasSize(1); + verify(instanceRepository).findByTypeAndSearch(InstanceType.PROXY, "prod"); + } + @Test void listInstancesShouldIgnoreBlankSearch() { List instances = List.of(InstanceVO.builder().name("inst").build()); diff --git a/server/src/test/java/org/apache/rocketmq/studio/instance/group/ConsumerDiagnosticsServiceTest.java b/server/src/test/java/org/apache/rocketmq/studio/instance/group/ConsumerDiagnosticsServiceTest.java index 034c1acd..6867871b 100644 --- a/server/src/test/java/org/apache/rocketmq/studio/instance/group/ConsumerDiagnosticsServiceTest.java +++ b/server/src/test/java/org/apache/rocketmq/studio/instance/group/ConsumerDiagnosticsServiceTest.java @@ -60,6 +60,24 @@ void getConsumerStackShouldDelegateToProvider() { verify(diagnosticsProvider).getConsumerStack("cg-orders", "client-1"); } + @Test + void getConsumerStackShouldTrimInputsBeforeDelegatingToProvider() { + ConsumerStackTraceVO stackTrace = ConsumerStackTraceVO.builder() + .groupName("cg-orders") + .clientId("client-1") + .capturedAt(LocalDateTime.now()) + .threadCount(0) + .threads(List.of()) + .build(); + when(diagnosticsProvider.getConsumerStack("cg-orders", "client-1")).thenReturn(stackTrace); + + ConsumerStackTraceVO result = diagnosticsService.getConsumerStack(" cg-orders ", " client-1 "); + + assertThat(result.getGroupName()).isEqualTo("cg-orders"); + assertThat(result.getClientId()).isEqualTo("client-1"); + verify(diagnosticsProvider).getConsumerStack("cg-orders", "client-1"); + } + @Test void getConsumerStackShouldRejectBlankGroupName() { assertThatThrownBy(() -> diagnosticsService.getConsumerStack(" ", "client-1")) diff --git a/server/src/test/java/org/apache/rocketmq/studio/instance/topic/MetadataServiceTest.java b/server/src/test/java/org/apache/rocketmq/studio/instance/topic/MetadataServiceTest.java index 21d30799..554335b7 100644 --- a/server/src/test/java/org/apache/rocketmq/studio/instance/topic/MetadataServiceTest.java +++ b/server/src/test/java/org/apache/rocketmq/studio/instance/topic/MetadataServiceTest.java @@ -68,6 +68,28 @@ void listTopicsShouldReturnEmptyWhenNone() { assertThat(result).isEmpty(); } + @Test + void listTopicsShouldNormalizeFiltersBeforeQueryingProvider() { + TopicVO topic = new TopicVO(); + topic.setName("order-topic"); + when(metadataProvider.listTopics("cluster-1", "FIFO", "order")).thenReturn(List.of(topic)); + + List result = metadataService.listTopics(" cluster-1 ", " FIFO ", " order "); + + assertThat(result).containsExactly(topic); + verify(metadataProvider).listTopics("cluster-1", "FIFO", "order"); + } + + @Test + void listTopicsShouldTreatBlankFiltersAsUnspecified() { + when(metadataProvider.listTopics(null, null, null)).thenReturn(List.of()); + + List result = metadataService.listTopics(" ", "\t", ""); + + assertThat(result).isEmpty(); + verify(metadataProvider).listTopics(null, null, null); + } + @Test void createTopicShouldDelegateToAdminClient() { TopicVO input = new TopicVO(); @@ -141,4 +163,16 @@ void listConsumerGroupsShouldPassSearchFilter() { assertThat(result).isEmpty(); verify(metadataProvider).listConsumerGroups(null, "order"); } + + @Test + void listConsumerGroupsShouldNormalizeFiltersBeforeQueryingProvider() { + ConsumerGroupVO group = new ConsumerGroupVO(); + group.setName("cg-order"); + when(metadataProvider.listConsumerGroups("cluster-1", "order")).thenReturn(List.of(group)); + + List result = metadataService.listConsumerGroups(" cluster-1 ", " order "); + + assertThat(result).containsExactly(group); + verify(metadataProvider).listConsumerGroups("cluster-1", "order"); + } } diff --git a/server/src/test/java/org/apache/rocketmq/studio/ops/audit/InMemoryAuditRepositoryTest.java b/server/src/test/java/org/apache/rocketmq/studio/ops/audit/InMemoryAuditRepositoryTest.java new file mode 100644 index 00000000..25516302 --- /dev/null +++ b/server/src/test/java/org/apache/rocketmq/studio/ops/audit/InMemoryAuditRepositoryTest.java @@ -0,0 +1,83 @@ +/* + * 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.ops.audit; + +import org.junit.jupiter.api.Test; +import org.springframework.test.util.ReflectionTestUtils; + +import java.time.LocalDateTime; +import java.util.Map; + +import static org.assertj.core.api.Assertions.assertThat; + +class InMemoryAuditRepositoryTest { + + private final InMemoryAuditRepository repository = new InMemoryAuditRepository(); + + @Test + void findAllShouldSearchAcrossNullableFields() { + AuditRecordVO missingTextFields = AuditRecordVO.builder() + .timestamp(LocalDateTime.now()) + .operationType("CREATE") + .result("SUCCESS") + .build(); + missingTextFields.setId("record-null-fields"); + AuditRecordVO targetMatch = AuditRecordVO.builder() + .timestamp(LocalDateTime.now().minusMinutes(1)) + .operator("admin") + .operationType("UPDATE") + .target("Topic-Order") + .detail(null) + .result("SUCCESS") + .build(); + targetMatch.setId("record-target-match"); + + putRecords(missingTextFields, targetMatch); + + assertThat(repository.findAll("order", null, null, null, null)) + .extracting(AuditRecordVO::getId) + .containsExactly("record-target-match"); + } + + @Test + void findAllShouldTreatBlankSearchAsNoSearchFilter() { + AuditRecordVO record = AuditRecordVO.builder() + .timestamp(LocalDateTime.now()) + .operationType("DELETE") + .result("FAILURE") + .build(); + record.setId("record-blank-search"); + + putRecords(record); + + assertThat(repository.findAll(" ", null, null, null, null)) + .extracting(AuditRecordVO::getId) + .containsExactly("record-blank-search"); + } + + @SafeVarargs + @SuppressWarnings("unchecked") + private final void putRecords(AuditRecordVO... records) { + Map store = + (Map) ReflectionTestUtils.getField(repository, "records"); + assertThat(store).isNotNull(); + store.clear(); + for (AuditRecordVO record : records) { + store.put(record.getId(), record); + } + } +} From 289c325529e91915f74911ffd0975c2f217ee483 Mon Sep 17 00:00:00 2001 From: liuhy Date: Sat, 25 Jul 2026 18:09:12 -0700 Subject: [PATCH 2/2] fix: make group search case insensitive --- web/src/pages/studio/GroupManagement.tsx | 7 ++++++- web/src/pages/studio/__tests__/GroupManagement.test.tsx | 2 +- 2 files changed, 7 insertions(+), 2 deletions(-) diff --git a/web/src/pages/studio/GroupManagement.tsx b/web/src/pages/studio/GroupManagement.tsx index 4c4976af..47102e16 100644 --- a/web/src/pages/studio/GroupManagement.tsx +++ b/web/src/pages/studio/GroupManagement.tsx @@ -227,6 +227,11 @@ const GroupManagementPage = () => { setModalVisible(true); }; + const normalizedSearchText = searchText.trim().toLowerCase(); + const filteredGroupData = groupData.filter( + (record) => !normalizedSearchText || record.group.toLowerCase().includes(normalizedSearchText), + ); + const columns = [ { title: t('groupMgmt.groupName'), @@ -390,7 +395,7 @@ const GroupManagementPage = () => { !searchText || d.group.includes(searchText))} + dataSource={filteredGroupData} pagination={{ pageSize: 10, showTotal: (total) => `${t('common.total')} ${total} Group`, diff --git a/web/src/pages/studio/__tests__/GroupManagement.test.tsx b/web/src/pages/studio/__tests__/GroupManagement.test.tsx index 3ac9fbbe..3b93a966 100644 --- a/web/src/pages/studio/__tests__/GroupManagement.test.tsx +++ b/web/src/pages/studio/__tests__/GroupManagement.test.tsx @@ -90,7 +90,7 @@ describe('GroupManagement Page', () => { const user = userEvent.setup(); renderWithProviders(); const searchInput = screen.getByPlaceholderText('搜索消费组'); - await user.type(searchInput, 'order'); + await user.type(searchInput, 'ORDER'); expect(screen.getByText('order-consumer-group')).toBeInTheDocument(); expect(screen.queryByText('payment-consumer-group')).not.toBeInTheDocument(); });