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 7d25628d4 fix(web): share one message timestamp formatter (#4829)
7d25628d4 is described below
commit 7d25628d432cc60cf40b01552090f568f65722a1
Author: Wang1rrr <[email protected]>
AuthorDate: Thu Oct 1 18:09:41 2026 +0800
fix(web): share one message timestamp formatter (#4829)
message.tsx and QueueBrowser.tsx each carried their own formatTimeMs for
the same MessageRecord.storeTime / trace node timestamp values, and the two
copies disagreed:
- the QueueBrowser one rendered toLocaleString('zh-CN'), so the queue
browser showed 2026/9/22 08:36:08 while every other timestamp in the
console is YYYY-MM-DD HH:mm:ss;
- the message page one started from "if (!value) return '-'", which treats
the Unix epoch as missing, and fed the raw value to new Date(), so an
unparseable timestamp rendered as NaN-NaN-NaN NaN:NaN:NaN.NaN instead of
the placeholder.
Move a single formatTimeMs into utils/format.ts, built on formatDateTime so
it inherits the console-wide layout, and have both call sites use it. Zero
is
a real timestamp; only an unusable value yields '-'.
---
web/src/components/QueueBrowser.tsx | 7 +----
web/src/components/__tests__/QueueBrowser.test.tsx | 31 +++++++++-------------
web/src/pages/instance/message.tsx | 9 +------
web/src/utils/format.test.ts | 26 ++++++++++++++++++
web/src/utils/format.ts | 16 +++++++++++
5 files changed, 57 insertions(+), 32 deletions(-)
diff --git a/web/src/components/QueueBrowser.tsx
b/web/src/components/QueueBrowser.tsx
index 3e4fe7f14..9d73e4181 100644
--- a/web/src/components/QueueBrowser.tsx
+++ b/web/src/components/QueueBrowser.tsx
@@ -35,6 +35,7 @@ import { CloseOutlined, SearchOutlined } from
'@ant-design/icons';
import type { MessageRecord, QueueOffset } from '../api/message';
import { getQueueOffsets, pullMessageAtOffset } from '../api/message';
import MessageProperties from './MessageProperties';
+import { formatTimeMs } from '../utils/format';
const { Text, Paragraph } = Typography;
@@ -43,12 +44,6 @@ export interface TopicOption {
value: string;
}
-export const formatTimeMs = (value: number | string) => {
- const ts = typeof value === 'string' ? Date.parse(value) : value;
- if (!Number.isFinite(ts)) return '-';
- return new Date(ts).toLocaleString('zh-CN', { hour12: false });
-};
-
export interface PulledEntry {
key: string;
offset: number;
diff --git a/web/src/components/__tests__/QueueBrowser.test.tsx
b/web/src/components/__tests__/QueueBrowser.test.tsx
index 9431c0b1e..5fbd0e679 100644
--- a/web/src/components/__tests__/QueueBrowser.test.tsx
+++ b/web/src/components/__tests__/QueueBrowser.test.tsx
@@ -20,7 +20,7 @@ import userEvent from '@testing-library/user-event';
import { beforeAll, beforeEach, describe, expect, it, vi } from 'vitest';
import type { MessageRecord, QueueOffset } from '../../api/message';
import { getQueueOffsets, pullMessageAtOffset } from '../../api/message';
-import { formatTimeMs, QueueBrowserResults, useQueueBrowser } from
'../QueueBrowser';
+import { QueueBrowserResults, useQueueBrowser } from '../QueueBrowser';
vi.mock('../../api/message', () => ({
getQueueOffsets: vi.fn(),
@@ -111,27 +111,20 @@ function QueueBrowserResultsProbe() {
const firstQueue = state.queues[0];
return (
<div>
- <button type="button" onClick={() =>
state.setTopic('topic-a')}>topic-a</button>
- <button type="button" onClick={() => void
state.loadQueues()}>load</button>
- <button type="button" onClick={() => firstQueue && void
state.handlePull(firstQueue)}>pull</button>
+ <button type="button" onClick={() => state.setTopic('topic-a')}>
+ topic-a
+ </button>
+ <button type="button" onClick={() => void state.loadQueues()}>
+ load
+ </button>
+ <button type="button" onClick={() => firstQueue && void
state.handlePull(firstQueue)}>
+ pull
+ </button>
<QueueBrowserResults state={state} />
</div>
);
}
-describe('formatTimeMs', () => {
- it('preserves the Unix epoch timestamp', () => {
- expect(formatTimeMs(0)).not.toBe('-');
- });
-
- it.each(['not-a-date', Number.NaN, Number.POSITIVE_INFINITY])(
- 'returns a placeholder for invalid timestamp %s',
- (value) => {
- expect(formatTimeMs(value)).toBe('-');
- },
- );
-});
-
describe('QueueBrowser request ownership', () => {
beforeEach(() => {
vi.clearAllMocks();
@@ -319,6 +312,8 @@ describe('QueueBrowser request ownership', () => {
const properties = await screen.findByRole('region', { name: '消息属性' });
expect(within(properties).getByText('queue-trace-123')).toBeInTheDocument();
-
expect(within(properties).getByText('属性过多或单值过长,服务端已截断展示')).toBeInTheDocument();
+ expect(
+ within(properties).getByText('属性过多或单值过长,服务端已截断展示'),
+ ).toBeInTheDocument();
});
});
diff --git a/web/src/pages/instance/message.tsx
b/web/src/pages/instance/message.tsx
index 31f90e6c2..db1df2ff9 100644
--- a/web/src/pages/instance/message.tsx
+++ b/web/src/pages/instance/message.tsx
@@ -74,7 +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 { formatBytes, formatTimeMs } from '../../utils/format';
import {
readMessageTraceTopic,
writeMessageTraceTopic,
@@ -121,13 +121,6 @@ const getDefaultRange = (): [Dayjs, Dayjs] =>
[dayjs().subtract(2, 'day').startO
/* ─── Helpers ─── */
-const formatTimeMs = (value: number | string): string => {
- if (!value) return '-';
- const d = new Date(value);
- const pad = (n: number, len = 2) => String(n).padStart(len, '0');
- return `${d.getFullYear()}-${pad(d.getMonth() + 1)}-${pad(d.getDate())}
${pad(d.getHours())}:${pad(d.getMinutes())}:${pad(d.getSeconds())}.${pad(d.getMilliseconds(),
3)}`;
-};
-
const formatBody = (body: string): string => {
try {
return JSON.stringify(JSON.parse(body), null, 2);
diff --git a/web/src/utils/format.test.ts b/web/src/utils/format.test.ts
index c7a06cee4..3e96edcc3 100644
--- a/web/src/utils/format.test.ts
+++ b/web/src/utils/format.test.ts
@@ -10,6 +10,7 @@ import {
formatPercent,
formatRelativeTime,
formatTimeOfDay,
+ formatTimeMs,
} from './format';
describe('formatBytes', () => {
@@ -97,3 +98,28 @@ describe('formatBytes', () => {
expect(formatTimeOfDay(now)).toBe('15:30');
});
});
+
+describe('formatTimeMs', () => {
+ const shape = /^\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2}\.\d{3}$/;
+
+ it('preserves the Unix epoch timestamp', () => {
+ expect(formatTimeMs(0)).not.toBe('-');
+ expect(formatTimeMs(0)).toMatch(shape);
+ });
+
+ it.each(['not-a-date', Number.NaN, Number.POSITIVE_INFINITY, null,
undefined, ''])(
+ 'returns a placeholder for an unusable timestamp %s',
+ (value) => {
+ expect(formatTimeMs(value)).toBe('-');
+ },
+ );
+
+ it('renders epoch milliseconds in the console-wide timestamp format', () => {
+ const timestamp = Date.parse('2026-07-31T00:00:00.123Z');
+ expect(formatTimeMs(timestamp)).toBe(`${formatDateTime(new
Date(timestamp))}.123`);
+ });
+
+ it('accepts the formatted strings cloud providers return', () => {
+ expect(formatTimeMs('2026-07-31T00:00:00Z')).toMatch(shape);
+ });
+});
diff --git a/web/src/utils/format.ts b/web/src/utils/format.ts
index ed5adbebe..2c291608a 100644
--- a/web/src/utils/format.ts
+++ b/web/src/utils/format.ts
@@ -32,6 +32,22 @@ export function formatDateTime(date: string | Date | null |
undefined): string {
);
}
+/**
+ * Format an epoch-millisecond value or a timestamp string as 'YYYY-MM-DD
HH:mm:ss.SSS'.
+ *
+ * Message store times and trace node timestamps arrive either as epoch
milliseconds (Apache
+ * brokers) or as formatted strings (cloud providers), so both are accepted.
Zero is a real
+ * timestamp rather than a missing one, and only an unusable value yields the
placeholder - a
+ * malformed timestamp has to read as absent instead of rendering as
NaN-NaN-NaN.
+ */
+export function formatTimeMs(value: number | string | null | undefined):
string {
+ if (value === null || value === undefined || value === '') return '-';
+ const timestamp = typeof value === 'string' ? Date.parse(value) : value;
+ if (!Number.isFinite(timestamp)) return '-';
+ const date = new Date(timestamp);
+ return `${formatDateTime(date)}.${pad(date.getMilliseconds(), 3)}`;
+}
+
export interface FormatUtcDateTimeOptions {
/**
* Append the viewer's short zone name (`GMT+8`). Defaults to true; pass
false where the zone is