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 85176a463 fix(dlq): drop the previous group messages when the detail 
drawer changes group (#4752)
85176a463 is described below

commit 85176a463ee43f0141f781abf45291528725b405
Author: 烤化の初雪 <[email protected]>
AuthorDate: Thu Sep 24 17:46:52 2026 +0800

    fix(dlq): drop the previous group messages when the detail drawer changes 
group (#4752)
    
    test(dlq): pin the in-flight window of the detail drawer switch
    
    The fix claims the drawer is empty for the group being opened both when
    that load fails and while it is still in flight; only the failure path
    had a case. Pin the in-flight window too, where the header has already
    switched while the request is still open.
    
    fix(dlq): drop the previous group messages when the detail drawer changes 
group
    
    Opening the message detail drawer for another DLQ group reused the rows
    and the total of the group that was open before: openDetailDrawer cleared
    only the page, the selection and the error. The instance switch and the
    drawer's own close handler both clear detailMessages/detailTotal, so the
    group switch was the one scope change that did not.
    
    A failed load for the second group then left the first group's messages
    rendered under the second group's title, with the export kept enabled by
    the stale total and the batch resend acting on those msgIds.
---
 web/src/pages/instance/__tests__/DLQPage.test.tsx | 96 +++++++++++++++++++++++
 web/src/pages/instance/dlq.tsx                    |  4 +
 2 files changed, 100 insertions(+)

diff --git a/web/src/pages/instance/__tests__/DLQPage.test.tsx 
b/web/src/pages/instance/__tests__/DLQPage.test.tsx
index b84e2a421..ebb44bf60 100644
--- a/web/src/pages/instance/__tests__/DLQPage.test.tsx
+++ b/web/src/pages/instance/__tests__/DLQPage.test.tsx
@@ -259,6 +259,102 @@ describe('DLQ page', () => {
     );
   });
 
+  it('drops the previous group messages when the next detail load fails', 
async () => {
+    
vi.mocked(messageService.listDLQGroups).mockResolvedValue(pageOf([dlqGroup, 
secondDlqGroup]));
+    vi.mocked(messageService.listDLQMessages)
+      .mockResolvedValueOnce({
+        items: [
+          {
+            msgId: 'order-dead-letter-1',
+            topic: 'orders',
+            queueId: 0,
+            offset: 11,
+            storeTime: 1_700_000_000_000,
+            keys: 'order-1',
+            body: 'dead',
+            bodyBase64: null,
+            properties: {},
+            propertiesTruncated: false,
+          },
+        ],
+        total: 1,
+        page: 1,
+        size: 20,
+      })
+      .mockRejectedValueOnce(new Error('broker unavailable'));
+
+    const user = userEvent.setup();
+    renderWithProviders(<DLQPage />);
+
+    const orderRow = (await screen.findByText('cg-order')).closest('tr');
+    if (!orderRow) throw new Error('DLQ group row not found');
+    await user.click(within(orderRow).getByRole('button', { name: /消息明细/ }));
+    expect(await screen.findByText('order-dead-letter-1')).toBeInTheDocument();
+
+    const paymentRow = (await 
screen.findByText('-cg-"payment"')).closest('tr');
+    if (!paymentRow) throw new Error('second DLQ group row not found');
+    await user.click(within(paymentRow).getByRole('button', { name: /消息明细/ }));
+
+    expect(await screen.findByText('DLQ 消息明细 · 
-cg-"payment"')).toBeInTheDocument();
+    expect(await screen.findByText('broker unavailable')).toBeInTheDocument();
+    // the drawer now belongs to another group, so the previous group's rows, 
its total and
+    // the export that is enabled from that total must not survive the failed 
load
+    expect(screen.queryByText('order-dead-letter-1')).not.toBeInTheDocument();
+    expect(screen.getByRole('button', { name: /导出全部/ })).toBeDisabled();
+  });
+
+  it('clears the previous group messages while the next detail load is in 
flight', async () => {
+    let resolveSecondDetail!: (page: DLQMessagePage) => void;
+    
vi.mocked(messageService.listDLQGroups).mockResolvedValue(pageOf([dlqGroup, 
secondDlqGroup]));
+    vi.mocked(messageService.listDLQMessages)
+      .mockResolvedValueOnce({
+        items: [
+          {
+            msgId: 'order-dead-letter-1',
+            topic: 'orders',
+            queueId: 0,
+            offset: 11,
+            storeTime: 1_700_000_000_000,
+            keys: 'order-1',
+            body: 'dead',
+            bodyBase64: null,
+            properties: {},
+            propertiesTruncated: false,
+          },
+        ],
+        total: 1,
+        page: 1,
+        size: 20,
+      })
+      .mockImplementationOnce(
+        () =>
+          new Promise<DLQMessagePage>((resolve) => {
+            resolveSecondDetail = resolve;
+          }),
+      );
+
+    const user = userEvent.setup();
+    renderWithProviders(<DLQPage />);
+
+    const orderRow = (await screen.findByText('cg-order')).closest('tr');
+    if (!orderRow) throw new Error('DLQ group row not found');
+    await user.click(within(orderRow).getByRole('button', { name: /消息明细/ }));
+    expect(await screen.findByText('order-dead-letter-1')).toBeInTheDocument();
+
+    const paymentRow = (await 
screen.findByText('-cg-"payment"')).closest('tr');
+    if (!paymentRow) throw new Error('second DLQ group row not found');
+    await user.click(within(paymentRow).getByRole('button', { name: /消息明细/ }));
+
+    // the drawer already belongs to the second group while its request is 
still open: the first
+    // group's rows and the total that enables the export must be gone before 
the response lands
+    expect(await screen.findByText('DLQ 消息明细 · 
-cg-"payment"')).toBeInTheDocument();
+    await waitFor(() => expect(screen.getByRole('button', { name: /导出全部/ 
})).toBeDisabled());
+    expect(screen.queryByText('order-dead-letter-1')).not.toBeInTheDocument();
+    expect(screen.queryByText('共 1 条消息')).not.toBeInTheDocument();
+
+    await act(async () => resolveSecondDetail({ items: [], total: 0, page: 1, 
size: 20 }));
+  });
+
   it('does not let an old-instance detail resend overwrite the new instance 
drawer', async () => {
     let resolveResend!: (result: DLQResendResult) => void;
     let resolveSecondDetail!: (page: DLQMessagePage) => void;
diff --git a/web/src/pages/instance/dlq.tsx b/web/src/pages/instance/dlq.tsx
index 21da027ee..165159427 100644
--- a/web/src/pages/instance/dlq.tsx
+++ b/web/src/pages/instance/dlq.tsx
@@ -377,6 +377,10 @@ const DLQPage = () => {
     setDetailGroup(group);
     setDetailOpen(true);
     setDetailPage(1);
+    // The drawer now belongs to another group: its rows, and the total the 
export and the
+    // pagination are driven by, must not survive from the group that was open 
before.
+    setDetailMessages([]);
+    setDetailTotal(0);
     setDetailSelectedMsgIds([]);
     setDetailError(null);
     void loadDetailMessages(group, 1, detailPageSize);

Reply via email to