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 c59de545f fix(web): reset pagination when filters change (#2434)
c59de545f is described below

commit c59de545f743f60ba76e29e119faf0164ab38c45
Author: btlqql <[email protected]>
AuthorDate: Fri Aug 21 18:03:53 2026 +0800

    fix(web): reset pagination when filters change (#2434)
---
 .../pages/cluster/__tests__/ClientsPage.test.tsx   | 19 +++++++++++
 web/src/pages/cluster/clients.tsx                  | 34 +++++++++++++++++---
 web/src/pages/studio/GroupManagement.tsx           | 17 ++++++++--
 web/src/pages/studio/LiteTopic.tsx                 | 19 +++++++++--
 .../studio/__tests__/GroupManagement.test.tsx      | 37 ++++++++++++++++++++++
 web/src/pages/studio/__tests__/LiteTopic.test.tsx  | 24 ++++++++++++++
 6 files changed, 141 insertions(+), 9 deletions(-)

diff --git a/web/src/pages/cluster/__tests__/ClientsPage.test.tsx 
b/web/src/pages/cluster/__tests__/ClientsPage.test.tsx
index b6b06a274..8573facb5 100644
--- a/web/src/pages/cluster/__tests__/ClientsPage.test.tsx
+++ b/web/src/pages/cluster/__tests__/ClientsPage.test.tsx
@@ -140,6 +140,25 @@ const renderWithProviders = (ui: React.ReactElement) =>
   );
 
 describe('Clients page', () => {
+  it('returns to the first page when the connection search changes', async () 
=> {
+    const pagedConnections = Array.from({ length: 21 }, (_, index) => ({
+      ...connection,
+      clientId: `client-${String(index).padStart(2, '0')}`,
+      address: `10.0.1.${index + 1}:49152`,
+    }));
+    
vi.mocked(connectionsService.listConnections).mockResolvedValue(pagedConnections);
+    const user = userEvent.setup();
+    const { container } = renderWithProviders(<ClientsPage />);
+
+    await screen.findByText('client-00');
+    await user.click(container.querySelector('.ant-pagination-next button')!);
+    expect(await screen.findByText('client-20')).toBeInTheDocument();
+    expect(screen.queryByText('client-00')).not.toBeInTheDocument();
+
+    await user.type(screen.getByPlaceholderText('搜索 Client ID 或地址'), 
'client-00');
+    expect(await screen.findByText('client-00')).toBeInTheDocument();
+  });
+
   it('loads connections for the first online broker cluster', async () => {
     renderWithProviders(<ClientsPage />);
 
diff --git a/web/src/pages/cluster/clients.tsx 
b/web/src/pages/cluster/clients.tsx
index 349b5265f..24700fcef 100644
--- a/web/src/pages/cluster/clients.tsx
+++ b/web/src/pages/cluster/clients.tsx
@@ -135,6 +135,8 @@ const ClientsPage = () => {
   const [registryLoadKey, setRegistryLoadKey] = useState(0);
   const [connectionLoadKey, setConnectionLoadKey] = useState(0);
   const [columnFilters, setColumnFilters] = useState<ClientTableFilters>({});
+  const [currentPage, setCurrentPage] = useState(1);
+  const [pageSize, setPageSize] = useState(20);
 
   const selectedCluster = registryClusters.find((cluster) => cluster.endpoint 
=== selectedEndpoint);
 
@@ -148,6 +150,7 @@ const ClientsPage = () => {
   );
 
   const handleNameserverChange = (endpoint: string) => {
+    setCurrentPage(1);
     setSelectedEndpoint(endpoint);
     setConnections([]);
     setClusterFilter('ALL');
@@ -282,6 +285,9 @@ const ClientsPage = () => {
     );
   }, [columnFilters, filtered]);
 
+  const lastPage = Math.max(1, Math.ceil(exportConnections.length / pageSize));
+  const clampedCurrentPage = Math.min(currentPage, lastPage);
+
   const handleExport = () => {
     const filename = `rocketmq-client-connections-${new 
Date().toISOString().slice(0, 10)}.csv`;
     const csv = buildCsv(CLIENT_CONNECTION_EXPORT_COLUMNS, exportConnections);
@@ -479,7 +485,10 @@ const ClientsPage = () => {
           <Select
             aria-label={t('clients.cluster')}
             value={clusterFilter}
-            onChange={setClusterFilter}
+            onChange={(value) => {
+              setClusterFilter(value);
+              setCurrentPage(1);
+            }}
             style={{ width: 180 }}
             options={clusterOptions}
           />
@@ -487,8 +496,14 @@ const ClientsPage = () => {
             placeholder={t('clients.searchPlaceholder')}
             allowClear
             value={search}
-            onChange={(e) => setSearch(e.target.value)}
-            onSearch={setSearch}
+            onChange={(e) => {
+              setSearch(e.target.value);
+              setCurrentPage(1);
+            }}
+            onSearch={(value) => {
+              setSearch(value);
+              setCurrentPage(1);
+            }}
             style={{ width: 280 }}
             prefix={<MagnifyingGlass size={14} color="#9CA3AF" />}
           />
@@ -572,10 +587,19 @@ const ClientsPage = () => {
             
`${connection.type}:${connection.clientId}:${connection.groupOrTopic}`
           }
           loading={loading}
-          onChange={(_, filters) => setColumnFilters(filters)}
+          onChange={(pagination, filters, _sorter, extra) => {
+            setColumnFilters(filters);
+            if (extra.action === 'filter') {
+              setCurrentPage(1);
+              return;
+            }
+            setCurrentPage(pagination.current ?? 1);
+            setPageSize(pagination.pageSize ?? 20);
+          }}
           scroll={{ x: tableScrollX(columns) }}
           pagination={{
-            pageSize: 20,
+            current: clampedCurrentPage,
+            pageSize,
             showSizeChanger: true,
             showTotal: (total) => `${t('common.total')} ${total}`,
           }}
diff --git a/web/src/pages/studio/GroupManagement.tsx 
b/web/src/pages/studio/GroupManagement.tsx
index 29038d6c4..e4125f581 100644
--- a/web/src/pages/studio/GroupManagement.tsx
+++ b/web/src/pages/studio/GroupManagement.tsx
@@ -71,6 +71,8 @@ const GroupManagementPage = () => {
   const [progressLoading, setProgressLoading] = useState(false);
   const [subscriptionError, setSubscriptionError] = useState<string | 
null>(null);
   const [progressError, setProgressError] = useState<string | null>(null);
+  const [currentPage, setCurrentPage] = useState(1);
+  const [pageSize, setPageSize] = useState(10);
   const listRequestId = useRef(0);
   const listInFlight = useRef<Promise<void> | null>(null);
   const listRefreshQueued = useRef(false);
@@ -188,6 +190,9 @@ const GroupManagementPage = () => {
     [groups, normalizedSearchText],
   );
 
+  const lastPage = Math.max(1, Math.ceil(filteredGroupData.length / pageSize));
+  const clampedCurrentPage = Math.min(currentPage, lastPage);
+
   const columns = [
     {
       title: t('groupMgmt.groupName'),
@@ -317,7 +322,10 @@ const GroupManagementPage = () => {
             placeholder={t('groupMgmt.searchPlaceholder')}
             prefix={<MagnifyingGlass size={14} />}
             value={searchText}
-            onChange={(e) => setSearchText(e.target.value)}
+            onChange={(e) => {
+              setSearchText(e.target.value);
+              setCurrentPage(1);
+            }}
             style={{ width: 240 }}
             allowClear
           />
@@ -346,7 +354,12 @@ const GroupManagementPage = () => {
           }
           loading={loading}
           pagination={{
-            pageSize: 10,
+            current: clampedCurrentPage,
+            pageSize,
+            onChange: (page, nextPageSize) => {
+              setCurrentPage(page);
+              setPageSize(nextPageSize);
+            },
             showTotal: (total) => `${t('common.total')} ${total} Group`,
             showSizeChanger: true,
           }}
diff --git a/web/src/pages/studio/LiteTopic.tsx 
b/web/src/pages/studio/LiteTopic.tsx
index 7692ad95d..4365045b7 100644
--- a/web/src/pages/studio/LiteTopic.tsx
+++ b/web/src/pages/studio/LiteTopic.tsx
@@ -113,6 +113,8 @@ const LiteTopicPage: React.FC = () => {
   const [namespaceFilter, setNamespaceFilter] = useState('');
   const [ttlStatusFilter, setTTLStatusFilter] = useState('');
   const [namespaceOptions, setNamespaceOptions] = useState<string[]>([]);
+  const [currentPage, setCurrentPage] = useState(1);
+  const [pageSize, setPageSize] = useState(10);
 
   // Session drawer
   const [sessionDrawerOpen, setSessionDrawerOpen] = useState(false);
@@ -240,6 +242,7 @@ const LiteTopicPage: React.FC = () => {
   }, [fetchData]);
 
   const handleSearch = () => {
+    setCurrentPage(1);
     void fetchData(patternFilter || undefined, namespaceFilter || undefined);
   };
 
@@ -250,6 +253,7 @@ const LiteTopicPage: React.FC = () => {
   const handleNamespaceChange = (val: string | undefined) => {
     const namespace = val || undefined;
     setNamespaceFilter(namespace || '');
+    setCurrentPage(1);
     void fetchData(patternFilter || undefined, namespace, { clear: true });
   };
 
@@ -314,6 +318,9 @@ const LiteTopicPage: React.FC = () => {
     return item.ttlStatus === ttlStatusFilter;
   });
 
+  const lastPage = Math.max(1, Math.ceil(filteredTopicList.length / pageSize));
+  const clampedCurrentPage = Math.min(currentPage, lastPage);
+
   // ─── Columns ─────────────────────────────────────────────────
 
   const columns: ColumnsType<LiteTopicItem> = [
@@ -756,7 +763,10 @@ const LiteTopicPage: React.FC = () => {
             aria-label={t('liteTopic.status')}
             placeholder={t('liteTopic.status')}
             value={ttlStatusFilter || undefined}
-            onChange={(value) => setTTLStatusFilter(value || '')}
+            onChange={(value) => {
+              setTTLStatusFilter(value || '');
+              setCurrentPage(1);
+            }}
             style={{ width: 160 }}
             allowClear
             options={[
@@ -780,7 +790,12 @@ const LiteTopicPage: React.FC = () => {
           rowKey={(record) => JSON.stringify([record.namespace, 
record.topicPattern])}
           loading={loading}
           pagination={{
-            pageSize: 10,
+            current: clampedCurrentPage,
+            pageSize,
+            onChange: (page, nextPageSize) => {
+              setCurrentPage(page);
+              setPageSize(nextPageSize);
+            },
             showTotal: (total) => t('liteTopic.total').replace('{total}', 
String(total)),
             showSizeChanger: true,
           }}
diff --git a/web/src/pages/studio/__tests__/GroupManagement.test.tsx 
b/web/src/pages/studio/__tests__/GroupManagement.test.tsx
index a42c6d804..ed4edffde 100644
--- a/web/src/pages/studio/__tests__/GroupManagement.test.tsx
+++ b/web/src/pages/studio/__tests__/GroupManagement.test.tsx
@@ -105,6 +105,43 @@ describe('GroupManagement Page', () => {
     vi.restoreAllMocks();
   });
 
+  it('returns to the first page when the group search changes', async () => {
+    vi.mocked(consumerService.listConsumerGroups).mockResolvedValue(
+      Array.from({ length: 11 }, (_, index) =>
+        makeGroup({ name: `group-${String(index).padStart(2, '0')}` }),
+      ),
+    );
+    const user = userEvent.setup();
+    const { container } = renderWithProviders(<GroupManagement />);
+
+    await screen.findByText('group-00');
+    await user.click(container.querySelector('.ant-pagination-next button')!);
+    expect(await screen.findByText('group-10')).toBeInTheDocument();
+    expect(screen.queryByText('group-00')).not.toBeInTheDocument();
+
+    await user.type(screen.getByPlaceholderText('搜索消费组'), 'group-00');
+    expect(await screen.findByText('group-00')).toBeInTheDocument();
+  });
+
+  it('clamps the current page when refreshed results have fewer pages', async 
() => {
+    const initialGroups = Array.from({ length: 21 }, (_, index) =>
+      makeGroup({ name: `group-${String(index).padStart(2, '0')}` }),
+    );
+    vi.mocked(consumerService.listConsumerGroups)
+      .mockResolvedValueOnce(initialGroups)
+      .mockResolvedValueOnce(initialGroups.slice(0, 11));
+    const user = userEvent.setup();
+    const { container } = renderWithProviders(<GroupManagement />);
+
+    await screen.findByText('group-00');
+    await user.click(container.querySelector('.ant-pagination-item-3 a')!);
+    expect(await screen.findByText('group-20')).toBeInTheDocument();
+
+    await user.click(screen.getByRole('button', { name: /刷新/ }));
+    expect(await screen.findByText('group-10')).toBeInTheDocument();
+    expect(screen.queryByText('group-20')).not.toBeInTheDocument();
+  });
+
   it('should render the page title', () => {
     renderWithProviders(<GroupManagement />);
     expect(screen.getByText('消费组管理')).toBeInTheDocument();
diff --git a/web/src/pages/studio/__tests__/LiteTopic.test.tsx 
b/web/src/pages/studio/__tests__/LiteTopic.test.tsx
index 6deaea9ae..b3c0aa665 100644
--- a/web/src/pages/studio/__tests__/LiteTopic.test.tsx
+++ b/web/src/pages/studio/__tests__/LiteTopic.test.tsx
@@ -103,6 +103,30 @@ describe('LiteTopic Page', () => {
     });
   });
 
+  it('returns to the first page when the local TTL filter changes', async () 
=> {
+    apiMocks.queryLiteTopicList.mockResolvedValue([
+      ...Array.from({ length: 11 }, (_, index) => ({
+        namespace: 'default',
+        topicPattern: `active-${String(index).padStart(2, '0')}*`,
+        ttlStatus: 'ACTIVE' as const,
+      })),
+      { namespace: 'default', topicPattern: 'expired-*', ttlStatus: 'EXPIRED' 
},
+    ]);
+    const user = userEvent.setup();
+    const { container } = renderPage();
+
+    await screen.findByText('active-00*');
+    await user.click(container.querySelector('.ant-pagination-next button')!);
+    expect(await screen.findByText('expired-*')).toBeInTheDocument();
+    expect(screen.queryByText('active-00*')).not.toBeInTheDocument();
+
+    await user.click(screen.getByRole('combobox', { name: '状态' }));
+    await user.click(
+      await screen.findByText('活跃', { selector: 
'.ant-select-item-option-content' }),
+    );
+    expect(await screen.findByText('active-00*')).toBeInTheDocument();
+  });
+
   it('displays the session POP progress returned by the API as a percentage', 
async () => {
     const user = userEvent.setup();
     renderPage();

Reply via email to