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 65d77f417 fix(web): drop the client heartbeat column the API can never 
fill (#7138)
65d77f417 is described below

commit 65d77f41738286560e80e22a161e13aebbeb3f93
Author: btlqql <[email protected]>
AuthorDate: Mon Oct 26 20:38:00 2026 +0800

    fix(web): drop the client heartbeat column the API can never fill (#7138)
    
    * fix(web): drop the client heartbeat column the API can never fill (#6086)
    The clients page advertised a connection time in three places -- the table's
    `Heartbeat` column, the CSV export's `Connected At` column and the 
connection detail
    dialog -- all bound to `ClientConnectionVO.connectedAt`, a field no 
provider ever
    assigns. `RocketMQClientProvider.toConnectionVO(ProducerInfo, ...)` 
(`:337`) and
    `toConnectionVO(Connection, ...)` (`:469`) build the VO from `ProducerInfo` 
/
    `Connection` payloads that carry no timestamp, and the proxy branch passes 
none
    either, so every cell rendered `-`. That reads as "this client never sent a
    heartbeat" rather than "this build does not collect it".
    Drop the three surfaces instead of inventing a timestamp the brokers do not 
report --
    the same resolution #5600 took for the fabricated NameServer `Connections` 
column.
    The regression test asserts that the column header, the dialog row and the 
CSV column
    are all gone, and the existing CSV/dialog expectations are trimmed 
accordingly.
    * fix(web): drop the i18n key the removed heartbeat column owned
    cluster.heartbeat labelled the connection-time column this PR removes; 
nothing
    references it afterwards, and an orphan key in the bilingual directory is 
exactly
    the drift the completeness guard cannot see.
---
 web/src/i18n/translations.ts                       |  1 -
 .../pages/cluster/__tests__/ClientsPage.test.tsx   | 39 ++++++++++++++++++++--
 web/src/pages/cluster/clients.tsx                  | 17 ----------
 3 files changed, 36 insertions(+), 21 deletions(-)

diff --git a/web/src/i18n/translations.ts b/web/src/i18n/translations.ts
index 4fe25240c..f244ec420 100644
--- a/web/src/i18n/translations.ts
+++ b/web/src/i18n/translations.ts
@@ -286,7 +286,6 @@ const translations: Record<string, Record<Lang, string>> = {
   'cluster.warning': { zh: '告警', en: 'Warning' },
   'cluster.error': { zh: '异常', en: 'Error' },
   'cluster.offline': { zh: '离线', en: 'Offline' },
-  'cluster.heartbeat': { zh: '心跳时间', en: 'Heartbeat' },
   'cluster.restartNotSupported': {
     zh: '暂不支持在线重启,请通过运维平台操作',
     en: 'Online restart not supported, please use ops platform',
diff --git a/web/src/pages/cluster/__tests__/ClientsPage.test.tsx 
b/web/src/pages/cluster/__tests__/ClientsPage.test.tsx
index 3aaa8e717..acee44de1 100644
--- a/web/src/pages/cluster/__tests__/ClientsPage.test.tsx
+++ b/web/src/pages/cluster/__tests__/ClientsPage.test.tsx
@@ -592,8 +592,8 @@ describe('Clients page', () => {
     const blob = createObjectURL.mock.calls[0][0] as Blob;
     await expect(blob.text()).resolves.toBe(
       [
-        '"Cluster","Client 
ID","Type","Group/Topic","Protocol","Address","Language","Version","Connected 
At","Partial"',
-        
'"ns-prod","\'=risky-client","Producer","order-create","gRPC","10.0.1.12:49152","Java","5.0.7","2026-07-01
 08:30:00","false"',
+        '"Cluster","Client 
ID","Type","Group/Topic","Protocol","Address","Language","Version","Partial"',
+        
'"ns-prod","\'=risky-client","Producer","order-create","gRPC","10.0.1.12:49152","Java","5.0.7","false"',
       ].join('\n'),
     );
     expect(
@@ -888,6 +888,39 @@ describe('Clients page', () => {
     expect(within(dialog).getByText('10.0.1.12:49152')).toBeInTheDocument();
     expect(within(dialog).getByText('Java')).toBeInTheDocument();
     expect(within(dialog).getByText('5.0.7')).toBeInTheDocument();
-    expect(within(dialog).getByText('2026-07-01 
08:30:00')).toBeInTheDocument();
+  });
+
+  // #6086: no provider (Apache remoting, proxy, or stub) can supply a 
connection time, so the page
+  // must not advertise one in the table, in the detail dialog or in the CSV 
export.
+  it('omits every connection-time control the API can never fill', async () => 
{
+    const createObjectURL = vi.fn((blob: Blob | MediaSource) => {
+      expect(blob).toBeInstanceOf(Blob);
+      return 'blob:connections-without-a-connection-time';
+    });
+    Object.defineProperty(URL, 'createObjectURL', {
+      writable: true,
+      value: createObjectURL,
+    });
+    Object.defineProperty(URL, 'revokeObjectURL', {
+      writable: true,
+      value: vi.fn(),
+    });
+    vi.spyOn(HTMLAnchorElement.prototype, 'click').mockImplementation(() => 
{});
+    const user = userEvent.setup({ pointerEventsCheck: 0 });
+    renderWithProviders(<ClientsPage />);
+
+    const row = await screen.findByRole('row', { name: 
/order-svc-0@10\.0\.1\.12:49152/ });
+    expect(screen.queryAllByText('心跳时间')).toHaveLength(0);
+
+    await user.click(screen.getByRole('button', { name: /导出/ }));
+    const csv = await (createObjectURL.mock.calls[0][0] as Blob).text();
+    expect(csv.split('\n')[0]).toBe(
+      '"Cluster","Client 
ID","Type","Group/Topic","Protocol","Address","Language","Version","Partial"',
+    );
+
+    await expectInteractive(within(row).getByRole('button', { name: /详情/ }));
+    await user.click(within(row).getByRole('button', { name: /详情/ }));
+    const dialog = await screen.findByRole('dialog', { name: /客户端详情/ });
+    expect(within(dialog).queryAllByText('心跳时间')).toHaveLength(0);
   });
 });
diff --git a/web/src/pages/cluster/clients.tsx 
b/web/src/pages/cluster/clients.tsx
index 65d0e261f..73aa5139f 100644
--- a/web/src/pages/cluster/clients.tsx
+++ b/web/src/pages/cluster/clients.tsx
@@ -44,7 +44,6 @@ import type { ClientConnection } from '../../api/connections';
 import { listConnections } from '../../services/connectionsService';
 import { listRegistryClusters } from '../../services/clusterService';
 import type { ClusterInfo } from '../../api/cluster';
-import { formatDateTime } from '../../utils/format';
 import { buildCsv, downloadCsv, localExportDateStamp, type CsvColumn } from 
'../../utils/download';
 import { describeThrownMessage } from '../../utils/apiError';
 import { tableScrollX } from '../../utils/table';
@@ -128,7 +127,6 @@ const CLIENT_CONNECTION_EXPORT_COLUMNS: 
CsvColumn<ClientConnection>[] = [
   { header: 'Address', value: (connection) => connection.address },
   { header: 'Language', value: (connection) => connection.language },
   { header: 'Version', value: (connection) => connection.version },
-  { header: 'Connected At', value: (connection) => connection.connectedAt },
   { header: 'Partial', value: (connection) => (connection.partial ? 'true' : 
'false') },
 ];
 
@@ -510,18 +508,6 @@ const ClientsPage = () => {
       width: 90,
       render: displayMetadata,
     },
-    {
-      title: t('cluster.heartbeat'),
-      dataIndex: 'connectedAt',
-      key: 'connectedAt',
-      width: 170,
-      sorter: (a, b) => (a.connectedAt ?? '').localeCompare(b.connectedAt ?? 
''),
-      render: (d?: string | null) => (
-        <Text type="secondary" style={{ fontSize: 14 }}>
-          {d ? formatDateTime(d) : '-'}
-        </Text>
-      ),
-    },
     {
       title: t('common.actions'),
       key: 'actions',
@@ -1039,9 +1025,6 @@ const ClientsPage = () => {
             <Descriptions.Item label={t('common.version')}>
               {displayMetadata(selectedConnection.version)}
             </Descriptions.Item>
-            <Descriptions.Item label={t('cluster.heartbeat')}>
-              {selectedConnection.connectedAt ?? '-'}
-            </Descriptions.Item>
           </Descriptions>
         )}
       </Modal>

Reply via email to