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 5648bf672 feat(message): show storage location and redelivery counts 
(#4267)
5648bf672 is described below

commit 5648bf6720cc40f14c8d8787ddfcd79ddcc65a48
Author: 烤化の初雪 <[email protected]>
AuthorDate: Wed Sep 16 15:55:51 2026 +0800

    feat(message): show storage location and redelivery counts (#4267)
    
    Incorporates two related fixes from the same author:
    
    - #4269 show the retry count on dead-letter messages
    - #4271 show the redelivery count on the message detail panel
---
 .../rocketmq/studio/instance/dlq/DLQMessageVO.java |  1 +
 .../studio/instance/message/MessageRecordVO.java   |  1 +
 .../provider/apache/RocketMQDLQProvider.java       |  1 +
 .../provider/apache/RocketMQMessageProvider.java   |  1 +
 .../provider/apache/RocketMQDLQProviderTest.java   | 22 +++++++
 .../apache/RocketMQMessageProviderTest.java        | 12 ++++
 web/src/api/message.ts                             |  2 +
 web/src/pages/instance/__tests__/DLQPage.test.tsx  | 33 ++++++++++
 .../pages/instance/__tests__/MessagePage.test.tsx  | 74 ++++++++++++++++++++++
 web/src/pages/instance/dlq.tsx                     | 11 +++-
 web/src/pages/instance/message.tsx                 | 12 ++++
 11 files changed, 169 insertions(+), 1 deletion(-)

diff --git 
a/server/src/main/java/org/apache/rocketmq/studio/instance/dlq/DLQMessageVO.java
 
b/server/src/main/java/org/apache/rocketmq/studio/instance/dlq/DLQMessageVO.java
index 7986b0455..9189cceb0 100644
--- 
a/server/src/main/java/org/apache/rocketmq/studio/instance/dlq/DLQMessageVO.java
+++ 
b/server/src/main/java/org/apache/rocketmq/studio/instance/dlq/DLQMessageVO.java
@@ -39,6 +39,7 @@ public class DLQMessageVO {
     private int queueId;
     private long offset;
     private long storeTime;
+    private int reconsumeTimes;
     private String keys;
     private String body;
     private String bodyBase64;
diff --git 
a/server/src/main/java/org/apache/rocketmq/studio/instance/message/MessageRecordVO.java
 
b/server/src/main/java/org/apache/rocketmq/studio/instance/message/MessageRecordVO.java
index 2dc53898e..8869f9b56 100644
--- 
a/server/src/main/java/org/apache/rocketmq/studio/instance/message/MessageRecordVO.java
+++ 
b/server/src/main/java/org/apache/rocketmq/studio/instance/message/MessageRecordVO.java
@@ -41,6 +41,7 @@ public class MessageRecordVO {
     private long storeTime;
     private String bornHost;
     private String storeHost;
+    private Integer reconsumeTimes;
     private Map<String, String> properties;
     private boolean propertiesTruncated;
     private int size;
diff --git 
a/server/src/main/java/org/apache/rocketmq/studio/provider/apache/RocketMQDLQProvider.java
 
b/server/src/main/java/org/apache/rocketmq/studio/provider/apache/RocketMQDLQProvider.java
index f3b32cad3..a3edd6fd1 100644
--- 
a/server/src/main/java/org/apache/rocketmq/studio/provider/apache/RocketMQDLQProvider.java
+++ 
b/server/src/main/java/org/apache/rocketmq/studio/provider/apache/RocketMQDLQProvider.java
@@ -419,6 +419,7 @@ public class RocketMQDLQProvider implements DLQProvider {
                 .queueId(message.getQueueId())
                 .offset(message.getQueueOffset())
                 .storeTime(message.getStoreTimestamp())
+                .reconsumeTimes(message.getReconsumeTimes())
                 .keys(message.getKeys())
                 .body(toUtf8Text(message.getBody()))
                 .bodyBase64(message.getBody() == null ? null
diff --git 
a/server/src/main/java/org/apache/rocketmq/studio/provider/apache/RocketMQMessageProvider.java
 
b/server/src/main/java/org/apache/rocketmq/studio/provider/apache/RocketMQMessageProvider.java
index ad98ae062..4b42fccaf 100644
--- 
a/server/src/main/java/org/apache/rocketmq/studio/provider/apache/RocketMQMessageProvider.java
+++ 
b/server/src/main/java/org/apache/rocketmq/studio/provider/apache/RocketMQMessageProvider.java
@@ -827,6 +827,7 @@ public class RocketMQMessageProvider implements 
MessageProvider {
                 .storeTime(messageExt.getStoreTimestamp())
                 .bornHost(String.valueOf(messageExt.getBornHost()))
                 .storeHost(String.valueOf(messageExt.getStoreHost()))
+                .reconsumeTimes(messageExt.getReconsumeTimes())
                 .properties(displayProperties)
                 .propertiesTruncated(properties != null && 
(displayProperties.size() < properties.size()
                         || 
MessagePropertyDisplay.hasOversizedProperty(properties)))
diff --git 
a/server/src/test/java/org/apache/rocketmq/studio/provider/apache/RocketMQDLQProviderTest.java
 
b/server/src/test/java/org/apache/rocketmq/studio/provider/apache/RocketMQDLQProviderTest.java
index 48986b883..01b780a3b 100644
--- 
a/server/src/test/java/org/apache/rocketmq/studio/provider/apache/RocketMQDLQProviderTest.java
+++ 
b/server/src/test/java/org/apache/rocketmq/studio/provider/apache/RocketMQDLQProviderTest.java
@@ -387,6 +387,28 @@ class RocketMQDLQProviderTest {
         assertThat(message.isPropertiesTruncated()).isTrue();
     }
 
+    @Test
+    void listMessagesShouldCarryReconsumeTimes() throws Exception {
+        String dlqTopic = MixAll.DLQ_GROUP_TOPIC_PREFIX + "group-a";
+        MessageQueue queue = new MessageQueue(dlqTopic, "broker-a", 0);
+        
when(pullConsumer.fetchSubscribeMessageQueues(dlqTopic)).thenReturn(Set.of(queue));
+        when(pullConsumer.searchOffset(eq(queue), anyLong())).thenReturn(0L);
+        MessageExt deadLetter = new MessageExt();
+        deadLetter.setMsgId("dlq-msg-retry");
+        deadLetter.setTopic("orders");
+        deadLetter.setStoreTimestamp(1_700_000_000_000L);
+        deadLetter.setBody("payload".getBytes(StandardCharsets.UTF_8));
+        deadLetter.setReconsumeTimes(3);
+        PullResult pullResult = new PullResult(PullStatus.FOUND, 1L, 0L, 0L, 
List.of(deadLetter));
+        when(pullConsumer.pull(eq(queue), eq("*"), anyLong(), 
anyInt())).thenReturn(pullResult);
+
+        PageResult<DLQMessageVO> page = provider.listMessages(
+                "instance-a", "group-a", 1_699_999_000_000L, 
1_700_100_000_000L, 1, 20);
+
+        assertThat(page.getItems()).hasSize(1);
+        assertThat(page.getItems().get(0).getReconsumeTimes()).isEqualTo(3);
+    }
+
     @Test
     void resendSelectedMessagesResolvesInTopologyMsgIdNormally() throws 
Exception {
         String dlqTopic = MixAll.DLQ_GROUP_TOPIC_PREFIX + "group-a";
diff --git 
a/server/src/test/java/org/apache/rocketmq/studio/provider/apache/RocketMQMessageProviderTest.java
 
b/server/src/test/java/org/apache/rocketmq/studio/provider/apache/RocketMQMessageProviderTest.java
index 50e9c0ba5..ba3d0acfa 100644
--- 
a/server/src/test/java/org/apache/rocketmq/studio/provider/apache/RocketMQMessageProviderTest.java
+++ 
b/server/src/test/java/org/apache/rocketmq/studio/provider/apache/RocketMQMessageProviderTest.java
@@ -655,6 +655,18 @@ class RocketMQMessageProviderTest {
         assertThat(record.isBodyTruncated()).isFalse();
     }
 
+    @Test
+    void toRecordVOCarriesReconsumeTimes() {
+        MessageExt message = new MessageExt();
+        message.setMsgId("msg-retry");
+        message.setTopic("TopicA");
+        message.setReconsumeTimes(4);
+
+        MessageRecordVO record = provider.toRecordVO(message);
+
+        assertThat(record.getReconsumeTimes()).isEqualTo(4);
+    }
+
     @Test
     void toRecordVODoesNotSplitUtf8CharacterAtBodyLimit() {
         MessageExt message = new MessageExt();
diff --git a/web/src/api/message.ts b/web/src/api/message.ts
index 3ce1539a1..fdf1ec74a 100644
--- a/web/src/api/message.ts
+++ b/web/src/api/message.ts
@@ -13,6 +13,7 @@ export interface MessageRecord {
   storeTime: number | string;
   bornHost: string;
   storeHost: string;
+  reconsumeTimes?: number;
   properties: Record<string, string>;
   size: number;
 }
@@ -114,6 +115,7 @@ export interface DLQMessage {
   queueId: number;
   offset: number;
   storeTime: number;
+  reconsumeTimes?: number;
   keys: string | null;
   body: string | null;
   bodyBase64: string | null;
diff --git a/web/src/pages/instance/__tests__/DLQPage.test.tsx 
b/web/src/pages/instance/__tests__/DLQPage.test.tsx
index d9324abdf..29cf71068 100644
--- a/web/src/pages/instance/__tests__/DLQPage.test.tsx
+++ b/web/src/pages/instance/__tests__/DLQPage.test.tsx
@@ -295,6 +295,39 @@ describe('DLQ page', () => {
     expect(screen.getByText('cn-east-1')).toBeInTheDocument();
   });
 
+  it('shows the retry count in the DLQ message drawer', async () => {
+    vi.mocked(messageService.listDLQMessages).mockResolvedValue({
+      items: [
+        {
+          msgId: 'dlq-retry',
+          topic: 'orders',
+          queueId: 2,
+          offset: 9,
+          storeTime: 1_700_000_000_000,
+          keys: 'key-retry',
+          body: 'payload',
+          bodyBase64: null,
+          properties: {},
+          propertiesTruncated: false,
+          reconsumeTimes: 3,
+        },
+      ],
+      total: 1,
+      page: 1,
+      size: 20,
+    } satisfies DLQMessagePage);
+    const user = userEvent.setup();
+    renderWithProviders(<DLQPage />);
+
+    await screen.findByText('cg-order');
+    await user.click(screen.getByRole('button', { name: /消息明细/ }));
+
+    expect((await 
screen.findAllByText('重投次数')).length).toBeGreaterThanOrEqual(1);
+    const keyCell = await screen.findByText('key-retry');
+    const row = keyCell.closest('tr') as HTMLElement;
+    expect(within(row).getByText('3')).toBeInTheDocument();
+  });
+
   it('exports the dead-letter messages of a group as Excel', async () => {
     vi.mocked(messageService.exportDLQExcel).mockResolvedValue({
       blob: new Blob(['xlsx-bytes'], {
diff --git a/web/src/pages/instance/__tests__/MessagePage.test.tsx 
b/web/src/pages/instance/__tests__/MessagePage.test.tsx
index f2dadaad5..af9be44fa 100644
--- a/web/src/pages/instance/__tests__/MessagePage.test.tsx
+++ b/web/src/pages/instance/__tests__/MessagePage.test.tsx
@@ -276,6 +276,30 @@ describe('Message page query history', () => {
     expect(messageServiceMocks.queryMessages).not.toHaveBeenCalled();
   });
 
+  it('shows the redelivery count on the message detail panel', async () => {
+    const user = userEvent.setup();
+    messageServiceMocks.queryMessages.mockResolvedValue([
+      { ...createMessage('MID-RETRY'), reconsumeTimes: 2 },
+    ]);
+    renderWithProviders(<MessagePage />);
+
+    await user.click(screen.getByText('按 Message ID'));
+    await user.click(lastElement(screen.getAllByRole('combobox')));
+    await user.click(lastElement(await screen.findAllByText('order-create')));
+    await user.type(screen.getByPlaceholderText('输入 Message ID'), 'MID-RETRY');
+    await user.click(screen.getByRole('button', { name: /^search查询$/ }));
+
+    expect(await screen.findByText('MID-RETRY')).toBeInTheDocument();
+    await user.click(screen.getByRole('button', { name: /详情/ }));
+
+    expect(await screen.findByText('消息体')).toBeInTheDocument();
+    const retryItems = screen
+      .getAllByText(/^重投次数$/)
+      .map((label) => label.closest('.ant-descriptions-item'));
+    expect(retryItems).toHaveLength(1);
+    expect(retryItems[0]).toHaveTextContent('2');
+  });
+
   it('loads topic options only for the selected instance', async () => {
     instanceFilterMocks.useInstanceFilter.mockReturnValue({
       selectedInstanceId: 1,
@@ -356,4 +380,54 @@ describe('Message page query history', () => {
       );
     });
   });
+
+  it('shows the storage location on the message detail panel', async () => {
+    const user = userEvent.setup();
+    
messageServiceMocks.queryMessages.mockResolvedValue([createMessage('MID-LOCATION')]);
+    renderWithProviders(<MessagePage />);
+
+    await user.click(screen.getByText('按 Message ID'));
+    await user.click(lastElement(screen.getAllByRole('combobox')));
+    await user.click(lastElement(await screen.findAllByText('order-create')));
+    await user.type(screen.getByPlaceholderText('输入 Message ID'), 
'MID-LOCATION');
+    await user.click(screen.getByRole('button', { name: /^search查询$/ }));
+
+    expect(await screen.findByText('MID-LOCATION')).toBeInTheDocument();
+    await user.click(screen.getByRole('button', { name: /详情/ }));
+
+    expect(await screen.findByText('消息体')).toBeInTheDocument();
+    const locationItems = screen
+      .getAllByText(/^(Broker|Queue ID|Queue Offset)$/)
+      .map((label) => label.closest('.ant-descriptions-item'));
+    expect(locationItems).toHaveLength(3);
+    expect(locationItems[0]).toHaveTextContent('broker-a');
+    expect(locationItems[1]).toHaveTextContent('0');
+    expect(locationItems[2]).toHaveTextContent('0');
+  });
+
+  it('renders placeholders on the detail panel when the storage location is 
unknown', async () => {
+    const user = userEvent.setup();
+    messageServiceMocks.queryMessages.mockResolvedValue([
+      { ...createMessage('MID-NO-LOCATION'), brokerName: null, queueId: null, 
queueOffset: null },
+    ]);
+    renderWithProviders(<MessagePage />);
+
+    await user.click(screen.getByText('按 Message ID'));
+    await user.click(lastElement(screen.getAllByRole('combobox')));
+    await user.click(lastElement(await screen.findAllByText('order-create')));
+    await user.type(screen.getByPlaceholderText('输入 Message ID'), 
'MID-NO-LOCATION');
+    await user.click(screen.getByRole('button', { name: /^search查询$/ }));
+
+    expect(await screen.findByText('MID-NO-LOCATION')).toBeInTheDocument();
+    await user.click(screen.getByRole('button', { name: /详情/ }));
+
+    expect(await screen.findByText('消息体')).toBeInTheDocument();
+    const locationItems = screen
+      .getAllByText(/^(Broker|Queue ID|Queue Offset)$/)
+      .map((label) => label.closest('.ant-descriptions-item'));
+    expect(locationItems).toHaveLength(3);
+    expect(locationItems[0]).toHaveTextContent('-');
+    expect(locationItems[1]).toHaveTextContent('-');
+    expect(locationItems[2]).toHaveTextContent('-');
+  });
 });
diff --git a/web/src/pages/instance/dlq.tsx b/web/src/pages/instance/dlq.tsx
index 9884c47e3..4ff539ec6 100644
--- a/web/src/pages/instance/dlq.tsx
+++ b/web/src/pages/instance/dlq.tsx
@@ -543,6 +543,15 @@ const DLQPage = () => {
       width: 90,
       render: (offset: number) => <Text style={{ fontFamily: 'monospace' 
}}>{offset}</Text>,
     },
+    {
+      title: '重投次数',
+      dataIndex: 'reconsumeTimes',
+      key: 'reconsumeTimes',
+      width: 90,
+      render: (reconsumeTimes?: number) => (
+        <Text style={{ fontFamily: 'monospace' }}>{reconsumeTimes ?? 
'-'}</Text>
+      ),
+    },
     {
       title: '入队时间',
       dataIndex: 'storeTime',
@@ -782,7 +791,7 @@ const DLQPage = () => {
          ═══════════════════════════════════════════ */}
       <Drawer
         title={detailGroup ? `DLQ 消息明细 · ${detailGroup.groupName}` : 'DLQ 
消息明细'}
-        width={1080}
+        width={1180}
         open={detailOpen}
         onClose={() => {
           detailRequestIdRef.current += 1;
diff --git a/web/src/pages/instance/message.tsx 
b/web/src/pages/instance/message.tsx
index a09b3049a..06457d794 100644
--- a/web/src/pages/instance/message.tsx
+++ b/web/src/pages/instance/message.tsx
@@ -894,6 +894,9 @@ const MessagePageContent = ({
               <span style={{ fontFamily: 'monospace' 
}}>{selectedMsg.key}</span>
             </Descriptions.Item>
             <Descriptions.Item 
label="大小">{formatSize(selectedMsg.size)}</Descriptions.Item>
+            <Descriptions.Item label="重投次数">
+              <span style={{ fontFamily: 'monospace' 
}}>{selectedMsg.reconsumeTimes ?? '-'}</span>
+            </Descriptions.Item>
             <Descriptions.Item label="Born Host">
               <span style={{ fontFamily: 'monospace' 
}}>{selectedMsg.bornHost}</span>
             </Descriptions.Item>
@@ -903,6 +906,15 @@ const MessagePageContent = ({
             <Descriptions.Item label="存储时间" span={2}>
               <span style={{ fontFamily: 'monospace' 
}}>{formatTimeMs(selectedMsg.storeTime)}</span>
             </Descriptions.Item>
+            <Descriptions.Item label="Broker">
+              <span style={{ fontFamily: 'monospace' 
}}>{selectedMsg.brokerName ?? '-'}</span>
+            </Descriptions.Item>
+            <Descriptions.Item label="Queue ID">
+              <span style={{ fontFamily: 'monospace' }}>{selectedMsg.queueId 
?? '-'}</span>
+            </Descriptions.Item>
+            <Descriptions.Item label="Queue Offset" span={2}>
+              <span style={{ fontFamily: 'monospace' 
}}>{selectedMsg.queueOffset ?? '-'}</span>
+            </Descriptions.Item>
           </Descriptions>
           <Typography.Title level={5} style={{ marginBottom: 8 }}>
             消息体

Reply via email to