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

Reply via email to