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 055d90882 fix(web): share one byte-size formatter for messages and 
payloads (#4834)
055d90882 is described below

commit 055d908829c5e46430a70fd4eb555b7e1cf93288
Author: Wang1rrr <[email protected]>
AuthorDate: Thu Oct 1 18:03:34 2026 +0800

    fix(web): share one byte-size formatter for messages and payloads (#4834)
    
    message.tsx and topic.tsx each carried a private byte formatter for the
    same MessageRecord.size and send-payload byte counts, and both copies
    stopped at megabytes:
    
      if (bytes >= 1048576) return `${(bytes / 1048576).toFixed(2)} MB`;
    
    so a 5 GiB message body rendered as "5120.00 MB" and a 2 TiB payload as
    "2097152.00 MB", while utils/format.ts already scales to PB. The copies
    also disagreed with the shared formatter on the base unit ("512 B" vs
    "512.0 B") and inherited none of its guards, so a size a provider could
    not resolve rendered as "NaN B".
    
    Delete both copies and point the five call sites at formatBytes. A byte
    count has no meaningful fraction, so the base unit now renders whole;
    the promotion loop has to round with the same width it renders with, or
    1023.6 would print "1024 B" instead of being promoted to "1.0 KB".
---
 .../pages/instance/__tests__/MessagePage.test.tsx  | 25 +++++++++++++++++++++-
 web/src/pages/instance/message.tsx                 | 11 +++-------
 web/src/pages/instance/topic.tsx                   |  8 +------
 web/src/utils/format.test.ts                       | 16 ++++++++++++++
 web/src/utils/format.ts                            | 10 +++++----
 5 files changed, 50 insertions(+), 20 deletions(-)

diff --git a/web/src/pages/instance/__tests__/MessagePage.test.tsx 
b/web/src/pages/instance/__tests__/MessagePage.test.tsx
index 5a9c02d83..80eae4481 100644
--- a/web/src/pages/instance/__tests__/MessagePage.test.tsx
+++ b/web/src/pages/instance/__tests__/MessagePage.test.tsx
@@ -398,7 +398,9 @@ describe('Message page query history', () => {
     expect(within(properties).getByText('traceId')).toBeInTheDocument();
     expect(within(properties).getByText('trace-123')).toBeInTheDocument();
     expect(within(properties).getByText('priority')).toBeInTheDocument();
-    
expect(within(properties).getByText('属性过多或单值过长,服务端已截断展示')).toBeInTheDocument();
+    expect(
+      within(properties).getByText('属性过多或单值过长,服务端已截断展示'),
+    ).toBeInTheDocument();
   });
 
   it('loads topic options only for the selected instance', async () => {
@@ -677,4 +679,25 @@ describe('Message page query history', () => {
     expect(locationItems[1]).toHaveTextContent('-');
     expect(locationItems[2]).toHaveTextContent('-');
   });
+
+  it('renders a message larger than a megabyte with the matching unit', async 
() => {
+    const user = userEvent.setup();
+    messageServiceMocks.queryMessages.mockResolvedValue([
+      { ...createMessage('MID-BIG-SIZE'), size: 5 * 1024 ** 3 },
+    ]);
+    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-BIG-SIZE');
+    await user.click(screen.getByRole('button', { name: /^search查询$/ }));
+
+    const row = await screen.findByRole('row', { name: /MID-BIG-SIZE/ });
+    expect(within(row).getByText('5.0 GB')).toBeInTheDocument();
+
+    await user.click(within(row).getByRole('button', { name: /详情/ }));
+    expect(await screen.findByText('消息体')).toBeInTheDocument();
+    expect(screen.getAllByText('5.0 GB').length).toBeGreaterThanOrEqual(2);
+  });
 });
diff --git a/web/src/pages/instance/message.tsx 
b/web/src/pages/instance/message.tsx
index 2e8608b3f..31f90e6c2 100644
--- a/web/src/pages/instance/message.tsx
+++ b/web/src/pages/instance/message.tsx
@@ -74,6 +74,7 @@ import { getInstanceCapabilities } from 
'../../services/instanceService';
 import { useInstanceFilter } from '../../hooks/useInstanceFilter';
 import { downloadBlob } from '../../utils/download';
 import { describeThrownMessage } from '../../utils/apiError';
+import { formatBytes } from '../../utils/format';
 import {
   readMessageTraceTopic,
   writeMessageTraceTopic,
@@ -120,12 +121,6 @@ const getDefaultRange = (): [Dayjs, Dayjs] => 
[dayjs().subtract(2, 'day').startO
 
 /* ─── Helpers ─── */
 
-const formatSize = (bytes: number): string => {
-  if (bytes >= 1048576) return `${(bytes / 1048576).toFixed(2)} MB`;
-  if (bytes >= 1024) return `${(bytes / 1024).toFixed(2)} KB`;
-  return `${bytes} B`;
-};
-
 const formatTimeMs = (value: number | string): string => {
   if (!value) return '-';
   const d = new Date(value);
@@ -842,7 +837,7 @@ const MessagePageContent = ({
       key: 'size',
       width: 80,
       align: 'right',
-      render: (size: number) => formatSize(size),
+      render: (size: number) => formatBytes(size),
     },
     {
       title: t('common.actions'),
@@ -958,7 +953,7 @@ const MessagePageContent = ({
               <span style={{ fontFamily: 'monospace' 
}}>{selectedMsg.key}</span>
             </Descriptions.Item>
             <Descriptions.Item label={t('messagePage.size')}>
-              {formatSize(selectedMsg.size)}
+              {formatBytes(selectedMsg.size)}
             </Descriptions.Item>
             <Descriptions.Item label={t('messagePage.reconsumeTimes')}>
               <span style={{ fontFamily: 'monospace' 
}}>{selectedMsg.reconsumeTimes ?? '-'}</span>
diff --git a/web/src/pages/instance/topic.tsx b/web/src/pages/instance/topic.tsx
index 44ad158f1..4ff97afb9 100644
--- a/web/src/pages/instance/topic.tsx
+++ b/web/src/pages/instance/topic.tsx
@@ -89,7 +89,7 @@ import {
 } from '../../utils/resourceCsvImport';
 import { isLagAvailable } from '../../utils/consumerLag';
 import { downloadCsv } from '../../utils/download';
-import { formatDateTime, formatNumber } from '../../utils/format';
+import { formatBytes, formatDateTime, formatNumber } from '../../utils/format';
 import { tableScrollX } from '../../utils/table';
 import {
   analyzeTopicRoutes,
@@ -307,12 +307,6 @@ const ISSUE_SEVERITY_COLOR: 
Record<RouteDiagnosticIssue['severity'], string> = {
 
 const formatPercent = (value: number) => `${value.toFixed(value % 1 === 0 ? 0 
: 1)}%`;
 
-const formatBytes = (bytes: number): string => {
-  if (bytes >= 1048576) return `${(bytes / 1048576).toFixed(2)} MB`;
-  if (bytes >= 1024) return `${(bytes / 1024).toFixed(2)} KB`;
-  return `${bytes} B`;
-};
-
 const BODY_FORMAT_LABEL: Record<MessageBodyFormat, string> = {
   empty: '空 Body',
   'json-object': 'JSON Object',
diff --git a/web/src/utils/format.test.ts b/web/src/utils/format.test.ts
index e76c8031b..c7a06cee4 100644
--- a/web/src/utils/format.test.ts
+++ b/web/src/utils/format.test.ts
@@ -35,6 +35,22 @@ describe('formatBytes', () => {
     expect(formatBytes(1024 * 1024 * 1024 - 1, 2)).toBe('1.00 GB');
     expect(formatBytes(1024 ** 6)).toBe('1024.0 PB');
   });
+
+  it('keeps scaling past megabytes instead of capping at MB', () => {
+    expect(formatBytes(5 * 1024 ** 3)).toBe('5.0 GB');
+    expect(formatBytes(2 * 1024 ** 4)).toBe('2.0 TB');
+  });
+
+  it('renders whole bytes because a byte count has no fraction', () => {
+    expect(formatBytes(1)).toBe('1 B');
+    expect(formatBytes(512)).toBe('512 B');
+    expect(formatBytes(512, 2)).toBe('512 B');
+    expect(formatBytes(-2048)).toBe('-2.0 KB');
+  });
+
+  it('promotes a sub-kilobyte value that only rounds up at whole-byte width', 
() => {
+    expect(formatBytes(1023.6)).toBe('1.0 KB');
+  });
   it('handles non-finite input', () => {
     expect(formatBytes(Number.NaN)).toBe('-');
     expect(formatBytes(Number.POSITIVE_INFINITY)).toBe('-');
diff --git a/web/src/utils/format.ts b/web/src/utils/format.ts
index defeee373..ed5adbebe 100644
--- a/web/src/utils/format.ts
+++ b/web/src/utils/format.ts
@@ -153,13 +153,15 @@ export function formatBytes(bytes: number, decimals = 1): 
string {
     value /= k;
     i += 1;
   }
-  // The unit is chosen from the unrounded value, so a value just below a 
boundary used to render
-  // as 1024.0 KB - a mantissa of 1024 that the loop above exists to avoid.
-  while (i < units.length - 1 && Number(value.toFixed(digits)) >= k) {
+  // A byte count has no meaningful fraction, so the base unit renders whole; 
every scaled unit keeps
+  // the requested precision. The promotion loop has to use the same width it 
renders with, or a
+  // value just below a boundary would round up to 1024 B instead of being 
promoted to 1.0 KB.
+  const precision = () => (i === 0 ? 0 : digits);
+  while (i < units.length - 1 && Number(value.toFixed(precision())) >= k) {
     value /= k;
     i += 1;
   }
-  return `${value.toFixed(digits)} ${units[i]}`;
+  return `${value.toFixed(precision())} ${units[i]}`;
 }
 
 /**

Reply via email to