You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@rocketmq.apache.org by li...@apache.org on 2026/10/19 12:53:06 UTC

(rocketmq-dashboard) branch rocketmq-studio updated: fix(export): date CSV filenames in the operator timezone (#6898)

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 97f668006 fix(export): date CSV filenames in the operator timezone (#6898)
97f668006 is described below

commit 97f668006bb0b8d6bb8e1f6a6fc661b961ad28b2
Author: Loyal-Young <hj...@gmail.com>
AuthorDate: Mon Oct 19 20:52:59 2026 +0800

    fix(export): date CSV filenames in the operator timezone (#6898)
    
    Verified against `rocketmq-studio` at a4dee573c before merging: the defect is still present on the
    current trunk and the change merges cleanly.
    
    Merged as-is (squash); no maintainer edits were needed.
    
    Locally verified: targeted suites green on the integration branch; a full backend and frontend
    build plus test pass runs on the trunk once this batch lands.
---
 web/src/pages/cluster/clients.tsx                   |  4 ++--
 web/src/pages/instance/consumer.tsx                 |  4 ++--
 web/src/pages/instance/topic.tsx                    |  4 ++--
 web/src/pages/ops/systemAlerts.tsx                  |  4 ++--
 web/src/pages/settings/CloudCredentialTab.tsx       |  4 ++--
 web/src/pages/settings/DataSourceTab.tsx            |  4 ++--
 .../settings/__tests__/CloudCredentialTab.test.tsx  |  3 ++-
 web/src/pages/studio/BrokerCluster.tsx              |  4 ++--
 web/src/pages/studio/LiteTopic.tsx                  |  4 ++--
 web/src/pages/studio/Producer.tsx                   |  4 ++--
 web/src/pages/studio/UserManagement.tsx             |  4 ++--
 web/src/utils/download.test.ts                      | 21 ++++++++++++++++++++-
 web/src/utils/download.ts                           |  5 +++++
 13 files changed, 47 insertions(+), 22 deletions(-)

diff --git a/web/src/pages/cluster/clients.tsx b/web/src/pages/cluster/clients.tsx
index 5e92a7eea..65d0e261f 100644
--- a/web/src/pages/cluster/clients.tsx
+++ b/web/src/pages/cluster/clients.tsx
@@ -45,7 +45,7 @@ import { listConnections } from '../../services/connectionsService';
 import { listRegistryClusters } from '../../services/clusterService';
 import type { ClusterInfo } from '../../api/cluster';
 import { formatDateTime } from '../../utils/format';
-import { buildCsv, downloadCsv, type CsvColumn } from '../../utils/download';
+import { buildCsv, downloadCsv, localExportDateStamp, type CsvColumn } from '../../utils/download';
 import { describeThrownMessage } from '../../utils/apiError';
 import { tableScrollX } from '../../utils/table';
 import {
@@ -382,7 +382,7 @@ const ClientsPage = () => {
   const clampedCurrentPage = Math.min(currentPage, lastPage);
 
   const handleExport = () => {
-    const filename = `rocketmq-client-connections-${new Date().toISOString().slice(0, 10)}.csv`;
+    const filename = `rocketmq-client-connections-${localExportDateStamp()}.csv`;
     const csv = buildCsv(CLIENT_CONNECTION_EXPORT_COLUMNS, exportConnections);
     downloadCsv(filename, csv);
   };
diff --git a/web/src/pages/instance/consumer.tsx b/web/src/pages/instance/consumer.tsx
index e68ef4029..1864c020c 100644
--- a/web/src/pages/instance/consumer.tsx
+++ b/web/src/pages/instance/consumer.tsx
@@ -103,7 +103,7 @@ import {
   validateConsumerGroupCsvImport,
   type ResourceImportRow,
 } from '../../utils/resourceCsvImport';
-import { downloadCsv } from '../../utils/download';
+import { downloadCsv, localExportDateStamp } from '../../utils/download';
 import { formatLag, isLagAvailable, lagSortValue } from '../../utils/consumerLag';
 import { formatOnlineInstances, onlineInstancesCount } from '../../utils/consumerConnections';
 import { tableScrollX } from '../../utils/table';
@@ -529,7 +529,7 @@ const ConsumerPageContent = ({
         search: search.trim() || undefined,
         subscriptionMode: modeFilter !== 'ALL' ? modeFilter : undefined,
       });
-      downloadCsv(`rocketmq-consumer-groups-${new Date().toISOString().slice(0, 10)}.csv`, csv);
+      downloadCsv(`rocketmq-consumer-groups-${localExportDateStamp()}.csv`, csv);
       message.success('Group 导出完成');
     } catch {
       message.error('导出 Group 失败,请稍后重试');
diff --git a/web/src/pages/instance/topic.tsx b/web/src/pages/instance/topic.tsx
index 081da44aa..76b6dd88e 100644
--- a/web/src/pages/instance/topic.tsx
+++ b/web/src/pages/instance/topic.tsx
@@ -84,7 +84,7 @@ import {
   type ResourceImportRow,
 } from '../../utils/resourceCsvImport';
 import { isLagAvailable } from '../../utils/consumerLag';
-import { downloadCsv } from '../../utils/download';
+import { downloadCsv, localExportDateStamp } from '../../utils/download';
 import { formatDateTime, formatNumber } from '../../utils/format';
 import { tableScrollX } from '../../utils/table';
 import {
@@ -684,7 +684,7 @@ const TopicPageContent = ({
       search: searchText.trim() || undefined,
     })
       .then((csv) => {
-        downloadCsv(`rocketmq-topics-${new Date().toISOString().slice(0, 10)}.csv`, csv);
+        downloadCsv(`rocketmq-topics-${localExportDateStamp()}.csv`, csv);
         message.success('Topic 导出完成');
       })
       .catch(() => {
diff --git a/web/src/pages/ops/systemAlerts.tsx b/web/src/pages/ops/systemAlerts.tsx
index d41785cc0..1812e35b9 100644
--- a/web/src/pages/ops/systemAlerts.tsx
+++ b/web/src/pages/ops/systemAlerts.tsx
@@ -62,7 +62,7 @@ import type {
   SystemAlert,
 } from '../../api/ops';
 import { formatUtcDateTime, formatNumber } from '../../utils/format';
-import { buildCsv, downloadCsv, type CsvColumn } from '../../utils/download';
+import { buildCsv, downloadCsv, localExportDateStamp, type CsvColumn } from '../../utils/download';
 import { zonedLocalDateTimeToUtc } from '../../utils/timeZone';
 
 const { Text } = Typography;
@@ -321,7 +321,7 @@ const SystemAlertsPage = () => {
         currentPage += 1;
       }
       downloadCsv(
-        `rocketmq-system-alerts-${new Date().toISOString().slice(0, 10)}.csv`,
+        `rocketmq-system-alerts-${localExportDateStamp()}.csv`,
         buildCsv(ALERT_EXPORT_COLUMNS, rows),
       );
       message.success(t('sysAlerts.exportSuccess', { count: rows.length }));
diff --git a/web/src/pages/settings/CloudCredentialTab.tsx b/web/src/pages/settings/CloudCredentialTab.tsx
index d16fe7b4c..83e2bdd34 100644
--- a/web/src/pages/settings/CloudCredentialTab.tsx
+++ b/web/src/pages/settings/CloudCredentialTab.tsx
@@ -45,7 +45,7 @@ import {
 } from '../../api/cloudCredential';
 import type { CloudCredential } from '../../api/cloudCredential';
 import type { InstanceVendor } from '../../api/instance';
-import { downloadBlob } from '../../utils/download';
+import { downloadBlob, localExportDateStamp } from '../../utils/download';
 
 const vendorTagColor: Record<string, string> = {
   ALIYUN: 'orange',
@@ -153,7 +153,7 @@ export const CloudCredentialTab = () => {
       const csv = await exportCloudCredentials(vendorFilter, debouncedSearch);
       downloadBlob(
         new Blob([csv], { type: 'text/csv;charset=utf-8' }),
-        `rocketmq-cloud-credentials-${new Date().toISOString().slice(0, 10)}.csv`,
+        `rocketmq-cloud-credentials-${localExportDateStamp()}.csv`,
       );
     } catch {
       message.error(t('settings.credentialExportFailed'));
diff --git a/web/src/pages/settings/DataSourceTab.tsx b/web/src/pages/settings/DataSourceTab.tsx
index 84f360010..e6de0b90d 100644
--- a/web/src/pages/settings/DataSourceTab.tsx
+++ b/web/src/pages/settings/DataSourceTab.tsx
@@ -48,7 +48,7 @@ import type { DataSource } from '../../api/settings';
 import { STATUS_MAP } from '../../constants/theme';
 import { listInstances } from '../../services/instanceService';
 import type { Instance } from '../../api/instance';
-import { buildCsv, downloadCsv, type CsvColumn } from '../../utils/download';
+import { buildCsv, downloadCsv, localExportDateStamp, type CsvColumn } from '../../utils/download';
 
 const { Text } = Typography;
 
@@ -326,7 +326,7 @@ export const DataSourceTab = () => {
         instanceNames: formatInstanceIds(source.instanceIds),
         statusLabel: formatStatus(source.status),
       }));
-      const filename = `rocketmq-data-sources-${new Date().toISOString().slice(0, 10)}.csv`;
+      const filename = `rocketmq-data-sources-${localExportDateStamp()}.csv`;
       downloadCsv(filename, buildCsv(DATA_SOURCE_EXPORT_COLUMNS, rows));
       message.success(t('settings.dataSourceExported', { total: rows.length }));
     } catch {
diff --git a/web/src/pages/settings/__tests__/CloudCredentialTab.test.tsx b/web/src/pages/settings/__tests__/CloudCredentialTab.test.tsx
index a75d176b6..10d1c6f30 100644
--- a/web/src/pages/settings/__tests__/CloudCredentialTab.test.tsx
+++ b/web/src/pages/settings/__tests__/CloudCredentialTab.test.tsx
@@ -40,7 +40,8 @@ vi.mock('../../../api/cloudCredential', () => ({
   updateCloudCredential: vi.fn(),
 }));
 
-vi.mock('../../../utils/download', () => ({
+vi.mock('../../../utils/download', async (importOriginal) => ({
+  ...(await importOriginal<typeof import('../../../utils/download')>()),
   downloadBlob: vi.fn(),
 }));
 
diff --git a/web/src/pages/studio/BrokerCluster.tsx b/web/src/pages/studio/BrokerCluster.tsx
index 6c07639aa..3696bb1f5 100644
--- a/web/src/pages/studio/BrokerCluster.tsx
+++ b/web/src/pages/studio/BrokerCluster.tsx
@@ -44,7 +44,7 @@ import type { ClusterInfo } from '../../api/cluster';
 import { supportsApacheRuntime, type Instance } from '../../api/instance';
 import { listInstances } from '../../services/instanceService';
 import { useVisiblePolling } from '../../hooks/useVisiblePolling';
-import { buildCsv, downloadCsv, type CsvColumn } from '../../utils/download';
+import { buildCsv, downloadCsv, localExportDateStamp, type CsvColumn } from '../../utils/download';
 import { roundPercent } from '../../utils/format';
 
 // ─── Types ──────────────────────────────────────────────────────
@@ -325,7 +325,7 @@ const BrokerClusterPage = () => {
   };
 
   function handleExport() {
-    const today = new Date().toISOString().slice(0, 10);
+    const today = localExportDateStamp();
     if (activeTab === 'nameserver') {
       downloadCsv(
         `rocketmq-nameserver-topology-${today}.csv`,
diff --git a/web/src/pages/studio/LiteTopic.tsx b/web/src/pages/studio/LiteTopic.tsx
index 64d1f519d..be301bb67 100644
--- a/web/src/pages/studio/LiteTopic.tsx
+++ b/web/src/pages/studio/LiteTopic.tsx
@@ -61,7 +61,7 @@ import {
   type LiteTopicSession,
 } from '../../api/liteTopic';
 import { listInstances, supportsApacheRuntime, type Instance } from '../../api/instance';
-import { buildCsv, downloadCsv, type CsvColumn } from '../../utils/download';
+import { buildCsv, downloadCsv, localExportDateStamp, type CsvColumn } from '../../utils/download';
 
 const formatDuration = (ms: number | undefined | null): string => {
   if (ms == null) return '-';
@@ -439,7 +439,7 @@ const LiteTopicPage: React.FC = () => {
         ttlStatusLabel: getTTLStatusLabel(item.ttlStatus),
         sessionCount: item.sessionIds?.length ?? 0,
       }));
-      const filename = `rocketmq-lite-topics-${new Date().toISOString().slice(0, 10)}.csv`;
+      const filename = `rocketmq-lite-topics-${localExportDateStamp()}.csv`;
       downloadCsv(filename, buildCsv(LITE_TOPIC_EXPORT_COLUMNS, rows));
       message.success(t('liteTopic.exportSuccess', { total: rows.length }));
     } catch {
diff --git a/web/src/pages/studio/Producer.tsx b/web/src/pages/studio/Producer.tsx
index 6177d120c..c606a2967 100644
--- a/web/src/pages/studio/Producer.tsx
+++ b/web/src/pages/studio/Producer.tsx
@@ -42,7 +42,7 @@ import {
 } from '../../api/producer';
 import { supportsApacheRuntime, type Instance } from '../../api/instance';
 import { listInstances } from '../../services/instanceService';
-import { buildCsv, downloadCsv, type CsvColumn } from '../../utils/download';
+import { buildCsv, downloadCsv, localExportDateStamp, type CsvColumn } from '../../utils/download';
 
 const readinessConfig: Record<ProducerReadiness, { color: string; type: 'success' | 'warning' }> = {
   READY: { color: 'success', type: 'success' },
@@ -314,7 +314,7 @@ const ProducerPage = () => {
       readiness: connectionSummary?.readiness,
       warnings,
     }));
-    const filename = `rocketmq-producer-connections-${new Date().toISOString().slice(0, 10)}.csv`;
+    const filename = `rocketmq-producer-connections-${localExportDateStamp()}.csv`;
     const csv = buildCsv(PRODUCER_CONNECTION_EXPORT_COLUMNS, rows);
     downloadCsv(filename, csv);
   };
diff --git a/web/src/pages/studio/UserManagement.tsx b/web/src/pages/studio/UserManagement.tsx
index 8b5897c5b..f5149bf91 100644
--- a/web/src/pages/studio/UserManagement.tsx
+++ b/web/src/pages/studio/UserManagement.tsx
@@ -60,7 +60,7 @@ import {
   type StudioUserSessionOverview,
 } from '../../api/studioUsers';
 import useAuthStore from '../../stores/authStore';
-import { buildCsv, downloadCsv, type CsvColumn } from '../../utils/download';
+import { buildCsv, downloadCsv, localExportDateStamp, type CsvColumn } from '../../utils/download';
 import { formatDelay, formatUtcDateTime } from '../../utils/format';
 import { tableScrollX } from '../../utils/table';
 import { useLang } from '../../i18n/LangContext';
@@ -379,7 +379,7 @@ const UserManagementPage = () => {
         admin: roleFilter === undefined ? undefined : roleFilter === 'admin',
         enabled: statusFilter === undefined ? undefined : statusFilter === 'enabled',
       });
-      const today = new Date().toISOString().slice(0, 10);
+      const today = localExportDateStamp();
       downloadCsv(
         `rocketmq-studio-users-${today}.csv`,
         buildCsv(STUDIO_USER_EXPORT_COLUMNS, exportedUsers),
diff --git a/web/src/utils/download.test.ts b/web/src/utils/download.test.ts
index 1b3be406a..f7ab86c4b 100644
--- a/web/src/utils/download.test.ts
+++ b/web/src/utils/download.test.ts
@@ -16,7 +16,26 @@
  */
 
 import { afterEach, describe, expect, it, vi } from 'vitest';
-import { buildCsv, downloadBlob, downloadCsv } from './download';
+import { buildCsv, downloadBlob, downloadCsv, localExportDateStamp } from './download';
+
+describe('localExportDateStamp', () => {
+  afterEach(() => vi.unstubAllEnvs());
+
+  it('uses the operator date when UTC is still on the previous day', () => {
+    vi.stubEnv('TZ', 'Asia/Shanghai');
+    const now = new Date('2026-10-16T16:30:00Z');
+    expect(now.toISOString().slice(0, 10)).toBe('2026-10-16');
+    expect(localExportDateStamp(now)).toBe('2026-10-17');
+  });
+
+  it('uses the operator date when UTC has already advanced to the next day', () => {
+    vi.stubEnv('TZ', 'America/Los_Angeles');
+    const now = new Date('2026-10-17T03:30:00Z');
+    expect(now.toISOString().slice(0, 10)).toBe('2026-10-17');
+    expect(localExportDateStamp(now)).toBe('2026-10-16');
+  });
+});
+
 describe('buildCsv', () => {
   it('escapes quotes, empty values, and spreadsheet formulas', () => {
     const csv = buildCsv(
diff --git a/web/src/utils/download.ts b/web/src/utils/download.ts
index 83af33d54..8ea1b7483 100644
--- a/web/src/utils/download.ts
+++ b/web/src/utils/download.ts
@@ -15,6 +15,11 @@
  * limitations under the License.
  */
 
+import { formatDate } from './format';
+
+/** Stamp export filenames in the operator's local date, including across a UTC date boundary. */
+export const localExportDateStamp = (now = new Date()): string => formatDate(now);
+
 export const downloadBlob = (blob: Blob, filename: string) => {
   const url = URL.createObjectURL(blob);
   const anchor = document.createElement('a');