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]}`;
}
/**