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 4707d54c5 fix(web): clamp out-of-range pages to the last valid page 
after deletes (#2841)
4707d54c5 is described below

commit 4707d54c5a3943a226841dd9b4387909498a4899
Author: cyberslack_lee <[email protected]>
AuthorDate: Wed Sep 2 16:26:08 2026 +0800

    fix(web): clamp out-of-range pages to the last valid page after deletes 
(#2841)
    
    clamp out-of-range pages to the last valid page after deletes
---
 web/src/pages/instance/__tests__/AclPage.test.tsx  | 43 ++++++++++++++++
 .../pages/instance/__tests__/ConsumerPage.test.tsx | 36 ++++++++++++++
 .../pages/instance/__tests__/TopicPage.test.tsx    | 57 ++++++++++++++++++++++
 web/src/pages/instance/acl.tsx                     |  6 +++
 web/src/pages/instance/consumer.tsx                |  3 ++
 web/src/pages/instance/topic.tsx                   |  3 ++
 6 files changed, 148 insertions(+)

diff --git a/web/src/pages/instance/__tests__/AclPage.test.tsx 
b/web/src/pages/instance/__tests__/AclPage.test.tsx
index f279794e4..a753c34c5 100644
--- a/web/src/pages/instance/__tests__/AclPage.test.tsx
+++ b/web/src/pages/instance/__tests__/AclPage.test.tsx
@@ -158,6 +158,49 @@ describe('ACL page', () => {
     
expect(within(accessKeyCell).queryByRole('button')).not.toBeInTheDocument();
   });
 
+  it('clamps rules back to a valid page when the current page becomes empty', 
async () => {
+    const user = userEvent.setup();
+    const ruleItems = (count: number) =>
+      Array.from({ length: count }, (_, index) => ({
+        id: index + 1,
+        principal: 'remote-user',
+        resource: `acl-topic-${String(index + 1).padStart(2, '0')}`,
+        resourceType: 'Topic',
+        resourcePattern: 'LITERAL',
+        actions: ['PUB'],
+        decision: 'ALLOW',
+        scope: 'cluster',
+        aclVersion: 2,
+        gmtCreate: '2026-07-23T00:00:00Z',
+      }));
+    let call = 0;
+    vi.mocked(aclService.listAclRules).mockImplementation(async (params) => {
+      call += 1;
+      if (params?.page === 2) {
+        // Page 2 went out of range (its rules were deleted server-side).
+        return { items: [], total: 15, page: 2, size: 20 };
+      }
+      // First load reports 45 rules (3 pages); the clamp re-fetch reports the 
shrunk 15.
+      return call === 1
+        ? { items: ruleItems(20), total: 45, page: 1, size: 20 }
+        : { items: ruleItems(15), total: 15, page: 1, size: 20 };
+    });
+    renderWithProviders(<AclPage />);
+
+    expect(await screen.findByText('acl-topic-01')).toBeInTheDocument();
+
+    const secondPage = document.querySelector('.ant-pagination-item-2');
+    expect(secondPage).not.toBeNull();
+    await user.click(secondPage as HTMLElement);
+
+    // The empty out-of-range page is corrected: the rules reload page 1.
+    await waitFor(() =>
+      expect(aclService.listAclRules).toHaveBeenLastCalledWith(
+        expect.objectContaining({ page: 1, pageSize: 20 }),
+      ),
+    );
+  });
+
   it('closes an ACL rule dialog when switching to another instance', async () 
=> {
     const user = userEvent.setup();
     vi.mocked(instanceService.listInstances).mockResolvedValue([
diff --git a/web/src/pages/instance/__tests__/ConsumerPage.test.tsx 
b/web/src/pages/instance/__tests__/ConsumerPage.test.tsx
index 55ba19b2c..12186cc05 100644
--- a/web/src/pages/instance/__tests__/ConsumerPage.test.tsx
+++ b/web/src/pages/instance/__tests__/ConsumerPage.test.tsx
@@ -91,6 +91,12 @@ const group: ConsumerGroup = {
   instances: [],
 };
 
+const buildGroups = (count: number): ConsumerGroup[] =>
+  Array.from({ length: count }, (_, index) => ({
+    ...group,
+    name: `remote-cg-${String(index + 1).padStart(2, '0')}`,
+  }));
+
 const groupPage = (
   items: ConsumerGroup[],
   overrides: Partial<{ total: number; page: number; size: number }> = {},
@@ -294,6 +300,36 @@ describe('Consumer page', () => {
     message.destroy();
   });
 
+  it('clamps back to a valid page when the current page becomes empty after a 
delete', async () => {
+    const user = userEvent.setup();
+    let call = 0;
+    vi.mocked(consumerService.listConsumerGroupPage).mockImplementation(async 
(params) => {
+      call += 1;
+      if (params?.page === 2) {
+        // Page 2 went out of range (its rows were deleted server-side).
+        return groupPage([], { total: 15, page: 2 });
+      }
+      // First load reports 45 rows (3 pages); the clamp re-fetch reports the 
shrunk 15.
+      return call === 1
+        ? groupPage(buildGroups(20), { total: 45, size: 20 })
+        : groupPage(buildGroups(15), { total: 15, size: 20 });
+    });
+    renderWithProviders(<ConsumerPage />);
+
+    expect(await screen.findByText('remote-cg-01')).toBeInTheDocument();
+
+    const secondPage = document.querySelector('.ant-pagination-item-2');
+    expect(secondPage).not.toBeNull();
+    await user.click(secondPage as HTMLElement);
+
+    // The empty out-of-range page is corrected: the list reloads page 1.
+    await waitFor(() =>
+      expect(consumerService.listConsumerGroupPage).toHaveBeenLastCalledWith(
+        expect.objectContaining({ page: 1, pageSize: 20 }),
+      ),
+    );
+  });
+
   it('submits the canonical global delivery order type', async () => {
     const user = userEvent.setup();
     const confirmSpy = vi.spyOn(Modal, 'confirm').mockImplementation((config) 
=> {
diff --git a/web/src/pages/instance/__tests__/TopicPage.test.tsx 
b/web/src/pages/instance/__tests__/TopicPage.test.tsx
index d2003577e..8dc37744b 100644
--- a/web/src/pages/instance/__tests__/TopicPage.test.tsx
+++ b/web/src/pages/instance/__tests__/TopicPage.test.tsx
@@ -324,6 +324,63 @@ describe('TopicPage', () => {
     
expect(within(getTableBody()).queryByText('topic-01')).not.toBeInTheDocument();
   });
 
+  it('clamps back to a valid page when the current page becomes empty after a 
delete', async () => {
+    const user = userEvent.setup();
+    instanceServiceMocks.listInstances.mockResolvedValue([
+      {
+        id: 6,
+        name: 'instance-a',
+        type: 'DIRECT',
+        endpoint: '127.0.0.1:9876',
+        remark: '',
+        topicCount: 45,
+        consumerGroupCount: 0,
+        gmtCreate: '2026-01-01T00:00:00Z',
+        gmtModified: '2026-01-01T00:00:00Z',
+      },
+    ]);
+    const requestedPages: number[] = [];
+    topicServiceMocks.listTopicsPage.mockImplementation(async (params) => {
+      requestedPages.push(params?.page ?? 0);
+      if (params?.page === 2) {
+        // Page 2 went out of range (its rows were deleted server-side):
+        // report no items and the shrunk total.
+        return { items: [], total: 15, page: 2, size: 20 };
+      }
+      // The first load reports 45 rows (3 pages); the clamp re-fetch
+      // reports the shrunk 15 rows that fit on a single page.
+      return requestedPages.length === 1
+        ? {
+            items: buildTopics(20).map((t) => ({ ...t, instanceId: 
'instance-a' })),
+            total: 45,
+            page: 1,
+            size: 20,
+          }
+        : {
+            items: buildTopics(15).map((t) => ({ ...t, instanceId: 
'instance-a' })),
+            total: 15,
+            page: 1,
+            size: 20,
+          };
+    });
+    renderWithProviders('/instance/instance-a/topic');
+
+    expect(await screen.findByText('topic-01')).toBeInTheDocument();
+
+    const secondPage = document.querySelector('.ant-pagination-item-2');
+    expect(secondPage).not.toBeNull();
+    await user.click(secondPage as HTMLElement);
+
+    // The empty out-of-range page is corrected: the table reloads the last 
valid page.
+    await waitFor(() =>
+      expect(topicServiceMocks.listTopicsPage).toHaveBeenLastCalledWith(
+        expect.objectContaining({ page: 1, pageSize: 20 }),
+      ),
+    );
+    expect(requestedPages).toEqual([1, 2, 1]);
+    expect(within(getTableBody()).getByText('topic-15')).toBeInTheDocument();
+  });
+
   it('keeps the selected instance when rebuilding a topic without a broker 
route', async () => {
     const user = userEvent.setup();
     const topic = { ...buildTopics(1)[0], instanceId: 'instance-a' };
diff --git a/web/src/pages/instance/acl.tsx b/web/src/pages/instance/acl.tsx
index bb08518bf..d08ab9884 100644
--- a/web/src/pages/instance/acl.tsx
+++ b/web/src/pages/instance/acl.tsx
@@ -197,6 +197,9 @@ const AclPageContent = ({
         if (!mounted) return;
         setRules(nextRules.items.map(normalizeRule));
         setRuleTotal(nextRules.total);
+        if (nextRules.items.length === 0 && nextRules.total > 0 && rulePage > 
1) {
+          setRulePage(Math.max(1, Math.ceil(nextRules.total / rulePageSize)));
+        }
       })
       .catch(() => {
         if (mounted) message.error(t('common.fetchDataFailed'));
@@ -215,6 +218,9 @@ const AclPageContent = ({
         if (mounted) {
           setUsers(result.items.map(normalizeUser));
           setUserTotal(result.total);
+          if (result.items.length === 0 && result.total > 0 && userPage > 1) {
+            setUserPage(Math.max(1, Math.ceil(result.total / userPageSize)));
+          }
         }
       })
       .catch(() => {
diff --git a/web/src/pages/instance/consumer.tsx 
b/web/src/pages/instance/consumer.tsx
index bed410356..e286603ce 100644
--- a/web/src/pages/instance/consumer.tsx
+++ b/web/src/pages/instance/consumer.tsx
@@ -331,6 +331,9 @@ const ConsumerPageContent = ({
           if (requestId === groupRequestIdRef.current) {
             setGroups(result.items);
             setTotalGroups(result.total);
+            if (result.items.length === 0 && result.total > 0 && page > 1) {
+              setPage(Math.max(1, Math.ceil(result.total / pageSize)));
+            }
           }
         })
         .catch(() => {
diff --git a/web/src/pages/instance/topic.tsx b/web/src/pages/instance/topic.tsx
index c2860ddd0..c45d78188 100644
--- a/web/src/pages/instance/topic.tsx
+++ b/web/src/pages/instance/topic.tsx
@@ -382,6 +382,9 @@ const TopicPage = () => {
           if (requestId === topicRequestIdRef.current) {
             setTopics(result.items);
             setTotalTopics(result.total);
+            if (result.items.length === 0 && result.total > 0 && tablePage > 
1) {
+              setTablePage(Math.max(1, Math.ceil(result.total / 
tablePageSize)));
+            }
           }
         })
         .catch(() => {

Reply via email to