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 7bbbea558 fix(consumer): filter the group page by subscription mode 
server-side (#7310)
7bbbea558 is described below

commit 7bbbea5581890c6ebe2a9e61d1e8b4c39f7cb745
Author: xdz <[email protected]>
AuthorDate: Mon Oct 26 17:29:47 2026 +0800

    fix(consumer): filter the group page by subscription mode server-side 
(#7310)
    
    * fix(consumer): filter the group page by subscription mode server-side
    Filters the consumer group page by subscription mode on the server side, 
instead of filtering only the currently loaded page in the browser.
    * fix(consumer): match the subscription mode filter to what the list 
displays
    parseSubscriptionMode answers Push for every stored value that is not Pop — 
a null,
    a blank, or a legacy "CLUSTERING" — but the paged query filtered with
    message_model = 'Push', so exactly those rows vanished from the filtered 
page while
    the unfiltered list still labelled them Push. Pop stays an equality; Push 
becomes
    "not Pop", and the requested mode is canonicalised so the in-memory 
inventory path
    and the SQL path cannot disagree on spelling.
    Cover the two shapes in the provider test, and document subscriptionMode on
    GET /api/groups/page.
---
 docs/api-spec.md                                   |  3 +-
 .../instance/group/ConsumerGroupController.java    |  4 +-
 .../studio/instance/topic/MetadataService.java     | 40 +++++++++++++++-
 .../rocketmq/studio/provider/InstanceProvider.java | 22 +++++++++
 .../provider/apache/ApacheInstanceProvider.java    |  7 +++
 .../studio/provider/apache/MetadataProvider.java   | 11 +++++
 .../provider/apache/RocketMQMetadataProvider.java  | 23 +++++++++
 .../group/ConsumerGroupControllerTest.java         | 19 +++++++-
 .../studio/instance/topic/MetadataServiceTest.java | 55 ++++++++++++++++++++--
 .../apache/RocketMQMetadataProviderTest.java       | 22 +++++++++
 web/src/api/metadata.ts                            |  3 +-
 .../pages/instance/__tests__/ConsumerPage.test.tsx | 21 +++++++++
 web/src/pages/instance/consumer.tsx                | 19 ++------
 web/src/services/consumerService.ts                |  6 +--
 14 files changed, 228 insertions(+), 27 deletions(-)

diff --git a/docs/api-spec.md b/docs/api-spec.md
index 481f489b2..01f5fceba 100644
--- a/docs/api-spec.md
+++ b/docs/api-spec.md
@@ -1435,7 +1435,7 @@ GET /api/groups/export?names={name1,name2}
 ### 6.10 分页获取消费组列表
 
 ```
-GET 
/api/groups/page?instanceId={instanceId}&clusterId={clusterId}&search={search}&page={page}&pageSize={pageSize}
+GET 
/api/groups/page?instanceId={instanceId}&clusterId={clusterId}&search={search}&subscriptionMode={subscriptionMode}&page={page}&pageSize={pageSize}
 ```
 
 **Query Parameters:**
@@ -1445,6 +1445,7 @@ GET 
/api/groups/page?instanceId={instanceId}&clusterId={clusterId}&search={searc
 | `instanceId` | `string` | 否 | 所属实例 ID,不传时按默认平台集群查询 |
 | `clusterId` | `string` | 否 | 按集群过滤 |
 | `search` | `string` | 否 | 按名称模糊搜索 |
+| `subscriptionMode` | `string` | 否 | 按订阅模式过滤:`Push` / `Pop`(大小写不敏感)。`Pop` 
为等值匹配;`Push` 与列表展示同口径,同时匹配 `message_model` 为空或存历史遗留值(如 `CLUSTERING`)的记录。不传或传 
`ALL` 表示不过滤 |
 | `page` | `number` | 否 | 页码,默认 `1` |
 | `pageSize` | `number` | 否 | 每页条数,默认 `20` |
 
diff --git 
a/server/src/main/java/org/apache/rocketmq/studio/instance/group/ConsumerGroupController.java
 
b/server/src/main/java/org/apache/rocketmq/studio/instance/group/ConsumerGroupController.java
index 1eb357f07..847eead74 100644
--- 
a/server/src/main/java/org/apache/rocketmq/studio/instance/group/ConsumerGroupController.java
+++ 
b/server/src/main/java/org/apache/rocketmq/studio/instance/group/ConsumerGroupController.java
@@ -56,9 +56,11 @@ public class ConsumerGroupController {
             @RequestParam(required = false) String instanceId,
             @RequestParam(required = false) String clusterId,
             @RequestParam(required = false) String search,
+            @RequestParam(required = false) String subscriptionMode,
             @RequestParam(defaultValue = "1") int page,
             @RequestParam(defaultValue = "20") int pageSize) {
-        return Result.ok(metadataService.listConsumerGroupsPage(instanceId, 
clusterId, search, page, pageSize));
+        return Result.ok(metadataService.listConsumerGroupsPage(instanceId, 
clusterId, search,
+                subscriptionMode, page, pageSize));
     }
 
     @GetMapping("/{name}")
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 039a45d5c..055439569 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
@@ -44,6 +44,7 @@ import 
org.apache.rocketmq.studio.common.domain.enums.SubscriptionMode;
 import org.apache.rocketmq.studio.common.exception.BusinessException;
 import org.apache.rocketmq.studio.common.util.CsvUtil;
 import org.apache.rocketmq.studio.common.util.MessagePropertyDisplay;
+import org.apache.rocketmq.studio.common.util.Pagination;
 import org.apache.rocketmq.studio.common.util.SystemTopicFilter;
 import org.apache.rocketmq.studio.instance.group.CreateConsumerGroupDTO;
 import org.apache.rocketmq.studio.instance.group.ImportConsumerGroupsResultVO;
@@ -504,14 +505,51 @@ public class MetadataService {
 
     public PageResult<ConsumerGroupVO> listConsumerGroupsPage(String 
instanceId, String clusterId, String search,
                                                               int page, int 
pageSize) {
+        return listConsumerGroupsPage(instanceId, clusterId, search, null, 
page, pageSize);
+    }
+
+    public PageResult<ConsumerGroupVO> listConsumerGroupsPage(String 
instanceId, String clusterId, String search,
+                                                              String 
subscriptionMode, int page, int pageSize) {
         validatePagination(page, pageSize);
         instanceId = normalizeInstanceId(instanceId);
+        String normalizedMode = normalizeSubscriptionMode(subscriptionMode);
         if (!StringUtils.hasText(instanceId) && 
StringUtils.hasText(clusterId)) {
+            // Legacy cluster-scoped read without an instance: filter the mode 
over the full
+            // inventory so the total describes the filtered result set, not 
one filtered page.
+            if (normalizedMode != null) {
+                List<ConsumerGroupVO> groups = 
metadataProvider.listConsumerGroups(
+                        normalizeFilter(clusterId), 
normalizeFilter(search)).stream()
+                        .filter(group -> group.getSubscriptionMode() != null
+                                && 
normalizedMode.equals(group.getSubscriptionMode().name()))
+                        .toList();
+                long offset = Pagination.pageOffset(page, pageSize);
+                int from = (int) Math.min(offset, groups.size());
+                int to = from + (int) Math.min(pageSize, groups.size() - from);
+                return PageResult.of(groups.subList(from, to), groups.size(), 
page, pageSize);
+            }
             return 
metadataProvider.listConsumerGroupsPage(normalizeFilter(clusterId),
                     normalizeFilter(search), page, pageSize);
         }
         return resolve(instanceId).listConsumerGroupsPage(instanceId, 
normalizeFilter(clusterId),
-                normalizeFilter(search), page, pageSize);
+                normalizeFilter(search), normalizedMode, page, pageSize);
+    }
+
+    /**
+     * Canonicalises the requested mode so the in-memory path over a full 
inventory and the SQL
+     * path over the page agree on the same spelling; an unknown value is 
passed through and simply
+     * matches nothing on both.
+     */
+    private static String normalizeSubscriptionMode(String subscriptionMode) {
+        if (!StringUtils.hasText(subscriptionMode) || 
"ALL".equals(subscriptionMode.trim())) {
+            return null;
+        }
+        String trimmed = subscriptionMode.trim();
+        for (SubscriptionMode mode : SubscriptionMode.values()) {
+            if (mode.name().equalsIgnoreCase(trimmed)) {
+                return mode.name();
+            }
+        }
+        return trimmed;
     }
 
 
diff --git 
a/server/src/main/java/org/apache/rocketmq/studio/provider/InstanceProvider.java
 
b/server/src/main/java/org/apache/rocketmq/studio/provider/InstanceProvider.java
index 9f33c6f8a..926892fe2 100644
--- 
a/server/src/main/java/org/apache/rocketmq/studio/provider/InstanceProvider.java
+++ 
b/server/src/main/java/org/apache/rocketmq/studio/provider/InstanceProvider.java
@@ -127,6 +127,28 @@ public interface InstanceProvider {
         return listConsumerGroupsPage(instanceId, search, page, pageSize);
     }
 
+    /**
+     * Instance-scoped consumer-group pagination with an optional 
subscription-mode filter
+     * ("Push" / "Pop"; blank or "ALL" means no restriction). Providers 
without a mode-aware
+     * store filter their full inventory before paginating, so the page and 
its total describe
+     * the filtered result set instead of one filtered page of an unfiltered 
count.
+     */
+    default PageResult<ConsumerGroupVO> listConsumerGroupsPage(String 
instanceId, String clusterId,
+            String search, String subscriptionMode, int page, int pageSize) {
+        if (subscriptionMode == null || subscriptionMode.isBlank() || 
"ALL".equals(subscriptionMode)) {
+            return listConsumerGroupsPage(instanceId, clusterId, search, page, 
pageSize);
+        }
+        List<ConsumerGroupVO> groups = listConsumerGroups(instanceId, 
search).stream()
+                .filter(group -> group.getSubscriptionMode() != null
+                        && 
subscriptionMode.equals(group.getSubscriptionMode().name()))
+                .toList();
+        int total = groups.size();
+        long offset = Pagination.pageOffset(page, pageSize);
+        int from = (int) Math.min(offset, total);
+        int to = from + (int) Math.min(pageSize, total - from);
+        return PageResult.of(groups.subList(from, to), total, page, pageSize);
+    }
+
     ConsumerGroupVO createConsumerGroup(String instanceId, ConsumerGroupVO 
group);
 
     /** Imports an existing consumer group; defaults to creation, providers 
may override to compare configurations. */
diff --git 
a/server/src/main/java/org/apache/rocketmq/studio/provider/apache/ApacheInstanceProvider.java
 
b/server/src/main/java/org/apache/rocketmq/studio/provider/apache/ApacheInstanceProvider.java
index f67d95b2a..7dbbef239 100644
--- 
a/server/src/main/java/org/apache/rocketmq/studio/provider/apache/ApacheInstanceProvider.java
+++ 
b/server/src/main/java/org/apache/rocketmq/studio/provider/apache/ApacheInstanceProvider.java
@@ -154,6 +154,13 @@ public class ApacheInstanceProvider implements 
InstanceProvider {
         return metadataProvider.listConsumerGroupsPage(instanceId, clusterId, 
search, page, pageSize);
     }
 
+    @Override
+    public PageResult<ConsumerGroupVO> listConsumerGroupsPage(String 
instanceId, String clusterId,
+            String search, String subscriptionMode, int page, int pageSize) {
+        return metadataProvider.listConsumerGroupsPage(instanceId, clusterId, 
search, subscriptionMode,
+                page, pageSize);
+    }
+
     @Override
     public ConsumerGroupVO createConsumerGroup(String instanceId, 
ConsumerGroupVO group) {
         requireGroupInstance(instanceId, group);
diff --git 
a/server/src/main/java/org/apache/rocketmq/studio/provider/apache/MetadataProvider.java
 
b/server/src/main/java/org/apache/rocketmq/studio/provider/apache/MetadataProvider.java
index 66454d34e..dae09e15f 100644
--- 
a/server/src/main/java/org/apache/rocketmq/studio/provider/apache/MetadataProvider.java
+++ 
b/server/src/main/java/org/apache/rocketmq/studio/provider/apache/MetadataProvider.java
@@ -71,6 +71,17 @@ public interface MetadataProvider {
         return listConsumerGroupsPage(clusterId, search, page, pageSize);
     }
 
+    /**
+     * Instance-scoped consumer-group pagination with an optional 
subscription-mode filter
+     * ("Push" / "Pop"; blank or "ALL" means no restriction). The default 
drops the filter;
+     * implementors with a mode-aware store override it so the page and its 
total describe the
+     * filtered result set.
+     */
+    default PageResult<ConsumerGroupVO> listConsumerGroupsPage(String 
instanceId, String clusterId,
+            String search, String subscriptionMode, int page, int pageSize) {
+        return listConsumerGroupsPage(instanceId, clusterId, search, page, 
pageSize);
+    }
+
     List<BrokerRouteVO> getTopicRoutes(String instanceId, String name);
     List<TopicConsumerVO> getTopicConsumers(String instanceId, String name);
 
diff --git 
a/server/src/main/java/org/apache/rocketmq/studio/provider/apache/RocketMQMetadataProvider.java
 
b/server/src/main/java/org/apache/rocketmq/studio/provider/apache/RocketMQMetadataProvider.java
index 2171f0550..acab23c8e 100644
--- 
a/server/src/main/java/org/apache/rocketmq/studio/provider/apache/RocketMQMetadataProvider.java
+++ 
b/server/src/main/java/org/apache/rocketmq/studio/provider/apache/RocketMQMetadataProvider.java
@@ -267,8 +267,19 @@ public class RocketMQMetadataProvider implements 
MetadataProvider {
     @Override
     public PageResult<ConsumerGroupVO> listConsumerGroupsPage(String 
instanceId, String clusterId,
             String search, int page, int pageSize) {
+        return listConsumerGroupsPage(instanceId, clusterId, search, null, 
page, pageSize);
+    }
+
+    @Override
+    public PageResult<ConsumerGroupVO> listConsumerGroupsPage(String 
instanceId, String clusterId,
+            String search, String subscriptionMode, int page, int pageSize) {
         String configuredCluster = StringUtils.hasText(instanceId)
                 ? runtimeAdminClientResolver.configuredClusterName(instanceId) 
: null;
+        // message_model stores the subscription mode ("Push"/"Pop"); blank 
and "ALL" mean no
+        // restriction, so the count and the page both describe the filtered 
result set.
+        boolean modeRestricted = StringUtils.hasText(subscriptionMode)
+                && !"ALL".equals(subscriptionMode);
+        boolean popOnly = modeRestricted && 
SubscriptionMode.Pop.name().equalsIgnoreCase(subscriptionMode);
         LambdaQueryWrapper<RmqGroup> query = new LambdaQueryWrapper<RmqGroup>()
                 .and(instanceId != null, scope -> {
                     scope.eq(RmqGroup::getInstanceId, 
normalizeMetadataScope(instanceId));
@@ -280,6 +291,18 @@ public class RocketMQMetadataProvider implements 
MetadataProvider {
                 })
                 .eq(StringUtils.hasText(clusterId), RmqGroup::getClusterId, 
clusterId)
                 .like(StringUtils.hasText(search), RmqGroup::getName, search)
+                .and(modeRestricted, scope -> {
+                    if (popOnly) {
+                        scope.eq(RmqGroup::getMessageModel, 
SubscriptionMode.Pop.name());
+                    } else {
+                        // parseSubscriptionMode answers Push for every value 
that is not Pop — a
+                        // null, a blank, or a legacy "CLUSTERING" — so the 
Push filter has to match
+                        // that same set. message_model = 'Push' would drop 
exactly the rows the
+                        // list shows as Push, and the page total would 
disagree with the page.
+                        scope.isNull(RmqGroup::getMessageModel)
+                                .or().ne(RmqGroup::getMessageModel, 
SubscriptionMode.Pop.name());
+                    }
+                })
                 .orderByAsc(RmqGroup::getName, RmqGroup::getId);
         Page<RmqGroup> result = groupMapper.selectPage(new Page<>(page, 
pageSize), query);
         List<ConsumerGroupVO> groups = result.getRecords().stream()
diff --git 
a/server/src/test/java/org/apache/rocketmq/studio/instance/group/ConsumerGroupControllerTest.java
 
b/server/src/test/java/org/apache/rocketmq/studio/instance/group/ConsumerGroupControllerTest.java
index 1e5226600..47c4bd6bd 100644
--- 
a/server/src/test/java/org/apache/rocketmq/studio/instance/group/ConsumerGroupControllerTest.java
+++ 
b/server/src/test/java/org/apache/rocketmq/studio/instance/group/ConsumerGroupControllerTest.java
@@ -87,7 +87,7 @@ class ConsumerGroupControllerTest extends 
WebMvcAuthTestSupport {
     @Test
     void listConsumerGroupsPageShouldPassSelectedInstanceFiltersAndPaging() 
throws Exception {
         PageResult<ConsumerGroupVO> page = PageResult.of(List.of(), 3, 2, 20);
-        when(metadataService.listConsumerGroupsPage("instance-a", "cluster-a", 
"orders", 2, 20))
+        when(metadataService.listConsumerGroupsPage("instance-a", "cluster-a", 
"orders", null, 2, 20))
                 .thenReturn(page);
 
         mockMvc.perform(get("/api/groups/page")
@@ -101,7 +101,22 @@ class ConsumerGroupControllerTest extends 
WebMvcAuthTestSupport {
                 .andExpect(jsonPath("$.data.page").value(2))
                 .andExpect(jsonPath("$.data.size").value(20));
 
-        verify(metadataService).listConsumerGroupsPage("instance-a", 
"cluster-a", "orders", 2, 20);
+        verify(metadataService).listConsumerGroupsPage("instance-a", 
"cluster-a", "orders", null, 2, 20);
+    }
+
+    @Test
+    void listConsumerGroupsPageShouldPassTheSubscriptionModeFilter() throws 
Exception {
+        PageResult<ConsumerGroupVO> page = PageResult.of(List.of(), 1, 1, 20);
+        when(metadataService.listConsumerGroupsPage("instance-a", null, null, 
"Push", 1, 20))
+                .thenReturn(page);
+
+        mockMvc.perform(get("/api/groups/page")
+                        .param("instanceId", "instance-a")
+                        .param("subscriptionMode", "Push"))
+                .andExpect(status().isOk())
+                .andExpect(jsonPath("$.data.total").value(1));
+
+        verify(metadataService).listConsumerGroupsPage("instance-a", null, 
null, "Push", 1, 20);
     }
 
     @Test
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 ddea630da..5276ecf2f 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
@@ -967,7 +967,7 @@ class MetadataServiceTest {
     void listConsumerGroupsPageShouldPaginateFromOneBasedIndexes() {
         ConsumerGroupVO third = new ConsumerGroupVO();
         third.setName("cg-c");
-        when(apacheProvider.listConsumerGroupsPage("instance-a", null, 
"order", 2, 2))
+        when(apacheProvider.listConsumerGroupsPage("instance-a", null, 
"order", null, 2, 2))
                 .thenReturn(PageResult.of(List.of(third), 3, 2, 2));
 
         PageResult<ConsumerGroupVO> result =
@@ -977,7 +977,7 @@ class MetadataServiceTest {
         assertThat(result.getTotal()).isEqualTo(3);
         assertThat(result.getPage()).isEqualTo(2);
         assertThat(result.getSize()).isEqualTo(2);
-        verify(apacheProvider).listConsumerGroupsPage("instance-a", null, 
"order", 2, 2);
+        verify(apacheProvider).listConsumerGroupsPage("instance-a", null, 
"order", null, 2, 2);
         verify(apacheProvider, 
org.mockito.Mockito.never()).listConsumerGroups("instance-a", "order");
     }
 
@@ -986,7 +986,7 @@ class MetadataServiceTest {
         ConsumerGroupVO clusterAGroup = new ConsumerGroupVO();
         clusterAGroup.setName("group-a");
         clusterAGroup.setClusterId("cluster-a");
-        when(apacheProvider.listConsumerGroupsPage("instance-a", "cluster-a", 
null, 1, 20))
+        when(apacheProvider.listConsumerGroupsPage("instance-a", "cluster-a", 
null, null, 1, 20))
                 .thenReturn(PageResult.of(List.of(clusterAGroup), 1, 1, 20));
 
         PageResult<ConsumerGroupVO> result = 
metadataService.listConsumerGroupsPage(
@@ -994,7 +994,54 @@ class MetadataServiceTest {
 
         assertThat(result.getItems()).containsExactly(clusterAGroup);
         assertThat(result.getTotal()).isEqualTo(1);
-        verify(apacheProvider).listConsumerGroupsPage("instance-a", 
"cluster-a", null, 1, 20);
+        verify(apacheProvider).listConsumerGroupsPage("instance-a", 
"cluster-a", null, null, 1, 20);
+    }
+
+    @Test
+    void listConsumerGroupsPageShouldPassSubscriptionModeToTheProviderTest() {
+        ConsumerGroupVO pushGroup = new ConsumerGroupVO();
+        pushGroup.setName("group-push");
+        when(apacheProvider.listConsumerGroupsPage("instance-a", null, null, 
"Push", 1, 20))
+                .thenReturn(PageResult.of(List.of(pushGroup), 1, 1, 20));
+
+        PageResult<ConsumerGroupVO> result = 
metadataService.listConsumerGroupsPage(
+                "instance-a", null, null, " Push ", 1, 20);
+
+        assertThat(result.getItems()).containsExactly(pushGroup);
+        verify(apacheProvider).listConsumerGroupsPage("instance-a", null, 
null, "Push", 1, 20);
+    }
+
+    @Test
+    void listConsumerGroupsPageShouldTreatAllAsNoSubscriptionModeTest() {
+        when(apacheProvider.listConsumerGroupsPage("instance-a", null, null, 
null, 1, 20))
+                .thenReturn(PageResult.empty(1, 20));
+
+        metadataService.listConsumerGroupsPage("instance-a", null, null, 
"ALL", 1, 20);
+
+        verify(apacheProvider).listConsumerGroupsPage("instance-a", null, 
null, null, 1, 20);
+    }
+
+    @Test
+    void 
listConsumerGroupsPageShouldFilterModeOverTheFullLegacyInventoryTest() {
+        ConsumerGroupVO pushGroup = new ConsumerGroupVO();
+        pushGroup.setName("group-push");
+        pushGroup.setSubscriptionMode(SubscriptionMode.Push);
+        ConsumerGroupVO popGroup = new ConsumerGroupVO();
+        popGroup.setName("group-pop");
+        popGroup.setSubscriptionMode(SubscriptionMode.Pop);
+        when(metadataProvider.listConsumerGroups("cluster-1", "order"))
+                .thenReturn(List.of(pushGroup, popGroup));
+
+        PageResult<ConsumerGroupVO> result = 
metadataService.listConsumerGroupsPage(
+                null, "cluster-1", "order", "Push", 2, 1);
+
+        // Only one Push row exists, so page 2 with size 1 is past the 
filtered total.
+        assertThat(result.getItems()).isEmpty();
+        assertThat(result.getTotal()).isEqualTo(1);
+        org.mockito.Mockito.verify(metadataProvider, 
org.mockito.Mockito.never())
+                
.listConsumerGroupsPage(org.mockito.ArgumentMatchers.anyString(),
+                        org.mockito.ArgumentMatchers.anyString(), 
org.mockito.ArgumentMatchers.anyInt(),
+                        org.mockito.ArgumentMatchers.anyInt());
     }
 
     @Test
diff --git 
a/server/src/test/java/org/apache/rocketmq/studio/provider/apache/RocketMQMetadataProviderTest.java
 
b/server/src/test/java/org/apache/rocketmq/studio/provider/apache/RocketMQMetadataProviderTest.java
index 441fe4a4c..4ce06642d 100644
--- 
a/server/src/test/java/org/apache/rocketmq/studio/provider/apache/RocketMQMetadataProviderTest.java
+++ 
b/server/src/test/java/org/apache/rocketmq/studio/provider/apache/RocketMQMetadataProviderTest.java
@@ -268,6 +268,28 @@ class RocketMQMetadataProviderTest {
         verify(runtimeAdminClientResolver, times(2)).execute(eq("instance-a"), 
any());
     }
 
+    @Test
+    void 
listConsumerGroupsPageShouldFilterTheSubscriptionModeTheWayItIsDisplayedTest() {
+        TableInfoHelper.initTableInfo(new MapperBuilderAssistant(new 
MybatisConfiguration(), ""), RmqGroup.class);
+        when(groupMapper.selectPage(any(Page.class), 
any(LambdaQueryWrapper.class)))
+                .thenReturn(new Page<>(1, 20, 0));
+        RocketMQMetadataProvider provider = newProvider();
+
+        provider.listConsumerGroupsPage("instance-a", null, null, "Pop", 1, 
20);
+        provider.listConsumerGroupsPage("instance-a", null, null, "Push", 1, 
20);
+
+        org.mockito.ArgumentCaptor<LambdaQueryWrapper<RmqGroup>> captor =
+                org.mockito.ArgumentCaptor.forClass(LambdaQueryWrapper.class);
+        verify(groupMapper, times(2)).selectPage(any(Page.class), 
captor.capture());
+        // Pop is a stored value, so it stays an equality. Push is what the 
read path shows for
+        // every row that is not Pop — a null, a blank, or a legacy 
"CLUSTERING" — so its filter
+        // has to match that set; message_model = 'Push' would hide those rows 
while the list
+        // still labels them Push.
+        
assertThat(captor.getAllValues().get(0).getSqlSegment()).contains("message_model
 =");
+        assertThat(captor.getAllValues().get(1).getSqlSegment())
+                .contains("message_model IS NULL", "message_model <>");
+    }
+
     @Test
     void getTopicRoutesShouldUseSelectedInstanceRuntimeClient() {
         List<BrokerRouteVO> routes = 
List.of(BrokerRouteVO.builder().brokerName("broker-a").build());
diff --git a/web/src/api/metadata.ts b/web/src/api/metadata.ts
index 92b1d9309..bfa08b729 100644
--- a/web/src/api/metadata.ts
+++ b/web/src/api/metadata.ts
@@ -174,6 +174,8 @@ export interface ConsumerGroupQuery {
   instanceId?: string;
   clusterId?: string;
   search?: string;
+  /** Subscription mode filter ("Push" / "Pop"); absent or "ALL" means no 
restriction. */
+  subscriptionMode?: string;
 }
 
 export interface ConsumerGroupPageQuery extends ConsumerGroupQuery {
@@ -183,7 +185,6 @@ export interface ConsumerGroupPageQuery extends 
ConsumerGroupQuery {
 
 export interface ConsumerGroupExportQuery extends ConsumerGroupQuery {
   names?: string[];
-  subscriptionMode?: string;
 }
 
 export interface ImportConsumerGroupsRequest {
diff --git a/web/src/pages/instance/__tests__/ConsumerPage.test.tsx 
b/web/src/pages/instance/__tests__/ConsumerPage.test.tsx
index 6e68ebfb8..6cfb89171 100644
--- a/web/src/pages/instance/__tests__/ConsumerPage.test.tsx
+++ b/web/src/pages/instance/__tests__/ConsumerPage.test.tsx
@@ -355,6 +355,27 @@ describe('Consumer page', () => {
     });
   });
 
+  it('applies the subscription-mode filter on the server', async () => {
+    const user = userEvent.setup({ pointerEventsCheck: 0 });
+    renderWithProviders(<ConsumerPage />);
+
+    expect(await screen.findByText('remote-cg')).toBeInTheDocument();
+
+    const modeSelect = screen.getAllByRole('combobox')[1];
+    fireEvent.mouseDown(modeSelect.parentElement!);
+    await user.click(await screen.findByText('Pop', { selector: 
'.ant-select-item-option-content' }));
+
+    await waitFor(() =>
+      expect(consumerService.listConsumerGroupPage).toHaveBeenLastCalledWith({
+        instanceId: 'instance-1',
+        page: 1,
+        pageSize: 20,
+        search: undefined,
+        subscriptionMode: 'Pop',
+      }),
+    );
+  });
+
   it('clears selected consumer groups when the search scope changes', async () 
=> {
     const user = userEvent.setup({ pointerEventsCheck: 0 });
     renderWithProviders(<ConsumerPage />);
diff --git a/web/src/pages/instance/consumer.tsx 
b/web/src/pages/instance/consumer.tsx
index 724180f19..8578a65dc 100644
--- a/web/src/pages/instance/consumer.tsx
+++ b/web/src/pages/instance/consumer.tsx
@@ -130,16 +130,6 @@ const lagColor = (lag: number): string => {
   return '#52c41a';
 };
 
-const visibleConsumerGroups = (groups: ConsumerGroup[], modeFilter: string): 
ConsumerGroup[] => {
-  let data = groups;
-
-  if (modeFilter !== 'ALL') {
-    data = data.filter((group) => group.subscriptionMode === modeFilter);
-  }
-
-  return data;
-};
-
 const normalizedConsistency = (value?: string | null): string => 
value?.trim().toLowerCase() ?? '';
 
 const isConsistentValue = (value?: string | null): boolean =>
@@ -353,6 +343,7 @@ const ConsumerPageContent = ({
         const result = await listConsumerGroupPage({
           instanceId: selectedInstanceId,
           search: search.trim() || undefined,
+          subscriptionMode: modeFilter !== 'ALL' ? modeFilter : undefined,
           page: pageToLoad,
           pageSize: pageSizeToLoad,
         });
@@ -376,7 +367,7 @@ const ConsumerPageContent = ({
         if (requestId === groupRequestIdRef.current) setLoading(false);
       }
     },
-    [t, selectedInstanceId, search],
+    [t, selectedInstanceId, search, modeFilter],
   );
 
   const reloadConsumerGroupPage = useCallback(async () => {
@@ -518,9 +509,9 @@ const ConsumerPageContent = ({
   }, [modalOpen, selectedGroupName, selectedInstanceId, loadProgress, 
loadSubscriptions]);
 
   /* ─── Filtered & sorted data ─── */
-  const filtered = useMemo(() => {
-    return visibleConsumerGroups(groups, modeFilter);
-  }, [groups, modeFilter]);
+  // The subscription-mode filter is applied server-side (it is part of the 
page query), so the
+  // loaded page is already the filtered inventory and the total matches what 
is displayed.
+  const filtered = useMemo(() => groups, [groups]);
 
   const handleExport = async () => {
     setExporting(true);
diff --git a/web/src/services/consumerService.ts 
b/web/src/services/consumerService.ts
index 53f57abdb..cd6a3edc0 100644
--- a/web/src/services/consumerService.ts
+++ b/web/src/services/consumerService.ts
@@ -102,6 +102,9 @@ function filterConsumerGroups(params?: ConsumerGroupQuery): 
ConsumerGroup[] {
     const kw = params.search.trim().toLowerCase();
     if (kw) result = result.filter((group) => 
group.name.toLowerCase().includes(kw));
   }
+  if (params?.subscriptionMode && params.subscriptionMode !== 'ALL') {
+    result = result.filter((group) => group.subscriptionMode === 
params.subscriptionMode);
+  }
   return result;
 }
 
@@ -111,9 +114,6 @@ function visibleConsumerGroups(groups: ConsumerGroup[], 
params?: ConsumerGroupEx
     const selectedNames = new Set(params.names);
     result = result.filter((group) => selectedNames.has(group.name));
   }
-  if (params?.subscriptionMode && params.subscriptionMode !== 'ALL') {
-    result = result.filter((group) => group.subscriptionMode === 
params.subscriptionMode);
-  }
   return [...result].sort((left, right) => 
left.name.localeCompare(right.name));
 }
 

Reply via email to