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 e8fc8b037 fix(web): warn on partially matched DLQ selected resends 
instead of success (#4912)
e8fc8b037 is described below

commit e8fc8b037a738905c6675f00d7c5af94f5a08550
Author: Apulupie <[email protected]>
AuthorDate: Mon Oct 19 19:57:35 2026 +0800

    fix(web): warn on partially matched DLQ selected resends instead of success 
(#4912)
    
    Verified against `rocketmq-studio` at 3bcd14d45 before merging: the defect 
is still present on the
    current trunk and the change merges cleanly.
    
    Maintainer follow-ups on top of the contributor commit:
    - fix(dlq): report unmatched and unscanned queues on the failed selected 
resend too
    
    Each follow-up commit message records the review finding it addresses.
    
    Locally verified: targeted suites green on the integration branch; a full 
backend and frontend
    build plus test pass runs on the trunk once this batch lands.
---
 web/src/i18n/translations.ts                      |   8 ++
 web/src/pages/instance/__tests__/DLQPage.test.tsx | 147 ++++++++++++++++++++++
 web/src/pages/instance/dlq.tsx                    |  27 ++++
 3 files changed, 182 insertions(+)

diff --git a/web/src/i18n/translations.ts b/web/src/i18n/translations.ts
index bed2185af..de81f7b05 100644
--- a/web/src/i18n/translations.ts
+++ b/web/src/i18n/translations.ts
@@ -911,6 +911,14 @@ const translations: Record<string, Record<Lang, string>> = 
{
     zh: '重投失败:成功 {resent},失败 {failed}',
     en: 'Resend failed: {resent} succeeded, {failed} failed',
   },
+  'dlq.resendUnmatchedSummary': {
+    zh: '重投不完整:成功 {resent},{unmatched} 条无法定位',
+    en: 'Resend incomplete: {resent} succeeded, {unmatched} could not be 
located',
+  },
+  'dlq.resendScanIncompleteSummary': {
+    zh: '重投不完整:成功 {resent},{queues} 个队列无法扫描',
+    en: 'Resend incomplete: {resent} succeeded, {queues} queues could not be 
scanned',
+  },
   'dlq.failureDetails': { zh: '失败消息明细', en: 'Failed message details' },
   'dlq.failureMessageId': { zh: '消息 ID', en: 'Message ID' },
   'dlq.failureTargetTopic': { zh: '目标 Topic', en: 'Target topic' },
diff --git a/web/src/pages/instance/__tests__/DLQPage.test.tsx 
b/web/src/pages/instance/__tests__/DLQPage.test.tsx
index 6b068a5ed..6fa46b1ee 100644
--- a/web/src/pages/instance/__tests__/DLQPage.test.tsx
+++ b/web/src/pages/instance/__tests__/DLQPage.test.tsx
@@ -1178,4 +1178,151 @@ describe('DLQ page', () => {
     expect(screen.queryByText('cg-order')).not.toBeInTheDocument();
     expect(screen.getByText('-cg-"payment"')).toBeInTheDocument();
   });
+
+  it('warns instead of celebrating when the selected resend could not match 
every message', async () => {
+    vi.mocked(messageService.listDLQMessages).mockResolvedValue({
+      items: [
+        {
+          msgId: 'dlq-gone',
+          topic: '%DLQ%cg-order',
+          queueId: 0,
+          offset: 7,
+          storeTime: 1_700_000_000_000,
+          keys: null,
+          body: 'payload',
+          bodyBase64: null,
+          properties: {},
+        },
+      ],
+      total: 1,
+      page: 1,
+      size: 20,
+    });
+    // The selection was captured before the messages were purged: the server 
reports matched=0,
+    // resent=0, failed=0 with outcome PARTIAL, and the old logic fell through 
to the success toast.
+    vi.mocked(messageService.resendDLQSelected).mockResolvedValue({
+      matched: 0,
+      resent: 0,
+      failed: 0,
+      outcome: 'PARTIAL',
+      scanIncomplete: false,
+      failedQueueCount: 0,
+    });
+    const user = userEvent.setup({ pointerEventsCheck: 0 });
+    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: /消息明细/ }));
+    const messageRow = (await screen.findByText('dlq-gone')).closest('tr');
+    if (!messageRow) throw new Error('DLQ message row not found');
+    await user.click(within(messageRow).getByRole('checkbox'));
+    await user.click(screen.getByRole('button', { name: /批量重发选中/ }));
+
+    expect(await screen.findByText(/1 条无法定位/)).toBeInTheDocument();
+    expect(screen.queryByText(/重发完成:成功 0 条/)).not.toBeInTheDocument();
+  });
+
+  it('warns when a queue could not be scanned for the selected resend', async 
() => {
+    vi.mocked(messageService.listDLQMessages).mockResolvedValue({
+      items: [
+        {
+          msgId: 'dlq-scanned',
+          topic: '%DLQ%cg-order',
+          queueId: 0,
+          offset: 9,
+          storeTime: 1_700_000_000_000,
+          keys: null,
+          body: 'payload',
+          bodyBase64: null,
+          properties: {},
+        },
+      ],
+      total: 1,
+      page: 1,
+      size: 20,
+    });
+    // The message matched, but one queue refused the scan: the caller has to 
hear that the result
+    // is incomplete, exactly like the time-range resend path reports it.
+    vi.mocked(messageService.resendDLQSelected).mockResolvedValue({
+      matched: 1,
+      resent: 0,
+      failed: 0,
+      outcome: 'PARTIAL',
+      scanIncomplete: true,
+      failedQueueCount: 2,
+    });
+    const user = userEvent.setup({ pointerEventsCheck: 0 });
+    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: /消息明细/ }));
+    const messageRow = (await screen.findByText('dlq-scanned')).closest('tr');
+    if (!messageRow) throw new Error('DLQ message row not found');
+    await user.click(within(messageRow).getByRole('checkbox'));
+    await user.click(screen.getByRole('button', { name: /批量重发选中/ }));
+
+    expect(await screen.findByText(/2 个队列无法扫描/)).toBeInTheDocument();
+    expect(screen.queryByText(/重发完成:成功 0 条/)).not.toBeInTheDocument();
+  });
+
+  it('keeps the unmatched warning when the selected resend also reports 
failures', async () => {
+    vi.mocked(messageService.listDLQMessages).mockResolvedValue({
+      items: [
+        {
+          msgId: 'dlq-failed',
+          topic: '%DLQ%cg-order',
+          queueId: 0,
+          offset: 11,
+          storeTime: 1_700_000_000_000,
+          keys: null,
+          body: 'payload',
+          bodyBase64: null,
+          properties: {},
+        },
+        {
+          msgId: 'dlq-unmatched',
+          topic: '%DLQ%cg-order',
+          queueId: 1,
+          offset: 12,
+          storeTime: 1_700_000_000_000,
+          keys: null,
+          body: 'payload',
+          bodyBase64: null,
+          properties: {},
+        },
+      ],
+      total: 2,
+      page: 1,
+      size: 20,
+    });
+    // Both rows are selected: one fails outright and one can no longer be 
resolved. The failure
+    // summary reports only resent + failed, so the unmatched count still has 
to be surfaced —
+    // this is the path the bare `failed > 0` branch used to swallow.
+    vi.mocked(messageService.resendDLQSelected).mockResolvedValue({
+      matched: 1,
+      resent: 0,
+      failed: 1,
+      outcome: 'PARTIAL',
+      scanIncomplete: true,
+      failedQueueCount: 3,
+    });
+    const user = userEvent.setup({ pointerEventsCheck: 0 });
+    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: /消息明细/ }));
+    for (const msgId of ['dlq-failed', 'dlq-unmatched']) {
+      const messageRow = (await screen.findByText(msgId)).closest('tr');
+      if (!messageRow) throw new Error(`DLQ message row not found: ${msgId}`);
+      await user.click(within(messageRow).getByRole('checkbox'));
+    }
+    await user.click(screen.getByRole('button', { name: /批量重发选中/ }));
+
+    expect(await screen.findByText(/重投部分完成:成功 0,失败 1/)).toBeInTheDocument();
+    expect(await screen.findByText(/1 条无法定位/)).toBeInTheDocument();
+    expect(screen.queryByText(/重发完成:成功 0 条/)).not.toBeInTheDocument();
+  });
 });
diff --git a/web/src/pages/instance/dlq.tsx b/web/src/pages/instance/dlq.tsx
index 566d2d780..354b54803 100644
--- a/web/src/pages/instance/dlq.tsx
+++ b/web/src/pages/instance/dlq.tsx
@@ -448,8 +448,35 @@ const DLQPage = () => {
         msgIds,
       });
       if (detailResendRequestIdRef.current !== requestId) return;
+      // The server reports `matched` separately from `resent`: a selected 
message that can no
+      // longer be resolved (purged, broker gone) is dropped with failed=0, 
and a queue that could
+      // not be scanned is reported separately too. Mirror the time-range 
resend handler: never show
+      // the success toast unless every selected message was matched and 
resent.
+      const unmatched = msgIds.length - result.matched;
       if (result.failed > 0) {
         showResendFailures(result);
+        // `showResendFailures` only reports resent + failed, so the 
partial-match information
+        // would be dropped on this path. Emit the same supplementary warning 
as the branches
+        // below, mirroring the time-range resend handler which also warns on 
top of the failure
+        // summary when its scan was incomplete.
+        if (unmatched > 0 || result.scanIncomplete) {
+          message.warning(
+            t(unmatched > 0 ? 'dlq.resendUnmatchedSummary' : 
'dlq.resendScanIncompleteSummary', {
+              resent: result.resent,
+              unmatched,
+              queues: result.failedQueueCount ?? 0,
+            }),
+          );
+        }
+      } else if (unmatched > 0) {
+        message.warning(t('dlq.resendUnmatchedSummary', { resent: 
result.resent, unmatched }));
+      } else if (result.scanIncomplete) {
+        message.warning(
+          t('dlq.resendScanIncompleteSummary', {
+            resent: result.resent,
+            queues: result.failedQueueCount ?? 0,
+          }),
+        );
       } else {
         message.success(`重发完成:成功 ${result.resent} 条`);
       }

Reply via email to