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 7d8f19363 fix(metrics): prevent pending queries from restoring cleared 
history (#6394)
7d8f19363 is described below

commit 7d8f19363c1d47bf689a2bb2904f2aa095c7ac8a
Author: Chstercc <[email protected]>
AuthorDate: Thu Oct 15 19:11:31 2026 +0800

    fix(metrics): prevent pending queries from restoring cleared history (#6394)
---
 web/src/components/MetricsExplorer.tsx             |  7 ++
 .../components/__tests__/MetricsExplorer.test.tsx  | 75 +++++++++++++++++++++-
 2 files changed, 81 insertions(+), 1 deletion(-)

diff --git a/web/src/components/MetricsExplorer.tsx 
b/web/src/components/MetricsExplorer.tsx
index ed547db36..2cbae78f0 100644
--- a/web/src/components/MetricsExplorer.tsx
+++ b/web/src/components/MetricsExplorer.tsx
@@ -547,6 +547,8 @@ const MetricsExplorer = ({ instanceId }: 
MetricsExplorerProps) => {
   // each other's in-flight results and leave every panel stuck on its spinner.
   const panelRequestIdRef = useRef(0);
   const customRequestIdRef = useRef(0);
+  // Clearing history invalidates prior queries only for history, not their 
chart results.
+  const historyGenerationRef = useRef(0);
   // Keeps the latest data source readable from the stable callbacks so 
switching
   // the source uses the new key instead of a stale closure value.
   const dataSourceKeyRef = useRef(dataSourceKey);
@@ -649,6 +651,7 @@ const MetricsExplorer = ({ instanceId }: 
MetricsExplorerProps) => {
     async (profile: MetricProfile | undefined, range: RangeOption) => {
       if (!profile) return;
       const currentRequest = ++panelRequestIdRef.current;
+      const historyGeneration = historyGenerationRef.current;
       const loadingPatch = Object.fromEntries(
         profile.metrics.map((metric) => [metric.semanticMetric, { loading: 
true } as PanelState]),
       );
@@ -691,6 +694,7 @@ const MetricsExplorer = ({ instanceId }: 
MetricsExplorerProps) => {
                 [metric.semanticMetric]: { loading: false, data: 
execution.data, query },
               }));
               setHistory((currentHistory) => {
+                if (historyGeneration !== historyGenerationRef.current) return 
currentHistory;
                 const nextHistory = mergeMetricsQueryHistory(currentHistory, 
historyEntry);
                 saveMetricsQueryHistory(nextHistory);
                 return nextHistory;
@@ -758,6 +762,7 @@ const MetricsExplorer = ({ instanceId }: 
MetricsExplorerProps) => {
       const trimmed = promql.trim();
       if (!trimmed) return;
       const currentRequest = ++customRequestIdRef.current;
+      const historyGeneration = historyGenerationRef.current;
       setCustomPanel({ loading: true });
       setAppliedCustomPromql(trimmed);
       try {
@@ -801,6 +806,7 @@ const MetricsExplorer = ({ instanceId }: 
MetricsExplorerProps) => {
           });
           setCustomPanel({ loading: false, data: execution.data, query });
           setHistory((currentHistory) => {
+            if (historyGeneration !== historyGenerationRef.current) return 
currentHistory;
             const nextHistory = mergeMetricsQueryHistory(currentHistory, 
historyEntry);
             saveMetricsQueryHistory(nextHistory);
             return nextHistory;
@@ -1120,6 +1126,7 @@ const MetricsExplorer = ({ instanceId }: 
MetricsExplorerProps) => {
   };
 
   const handleClearHistory = () => {
+    historyGenerationRef.current += 1;
     clearMetricsQueryHistory();
     setHistory([]);
   };
diff --git a/web/src/components/__tests__/MetricsExplorer.test.tsx 
b/web/src/components/__tests__/MetricsExplorer.test.tsx
index 246965c0c..be438a7dc 100644
--- a/web/src/components/__tests__/MetricsExplorer.test.tsx
+++ b/web/src/components/__tests__/MetricsExplorer.test.tsx
@@ -16,7 +16,7 @@
  */
 
 import { App, ConfigProvider } from 'antd';
-import { fireEvent, render, screen, waitFor, within } from 
'@testing-library/react';
+import { act, fireEvent, render, screen, waitFor, within } from 
'@testing-library/react';
 import userEvent from '@testing-library/user-event';
 import type React from 'react';
 import { afterEach, beforeAll, beforeEach, describe, expect, it, vi } from 
'vitest';
@@ -1219,6 +1219,79 @@ describe('MetricsExplorer', () => {
     expect(within(detailsDialog).getByText('42')).toBeInTheDocument();
   });
 
+  it.each(['profile', 'custom'] as const)(
+    'doesNotRestoreClearedHistoryWhenAPending%sQueryCompletesTest',
+    async (kind) => {
+      const user = userEvent.setup();
+      renderWithProviders(<MetricsExplorer />);
+      await screen.findByRole('img', { name: 'Message In TPS time series' });
+      const pending = createDeferred<MetricData>();
+      vi.mocked(queryMetrics).mockReturnValueOnce(pending.promise);
+
+      if (kind === 'profile') {
+        await user.click(screen.getByRole('button', { name: '刷新全部面板' }));
+      } else {
+        await user.type(screen.getByLabelText('自定义查询'), 
'sum(rocketmq_topic_number)');
+        await user.click(screen.getByRole('button', { name: '查询' }));
+      }
+      await waitFor(() => expect(queryMetrics).toHaveBeenCalledTimes(2));
+      await user.click(screen.getByRole('button', { name: '查询历史' }));
+      const drawer = (await screen.findByText('指标查询历史')).closest(
+        '[role="dialog"]',
+      ) as HTMLElement;
+      await user.click(within(drawer).getByRole('button', { name: '清空历史' }));
+      
expect(localStorage.getItem(METRICS_QUERY_HISTORY_STORAGE_KEY)).toBeNull();
+
+      await act(async () => pending.resolve(metricData));
+      expect(
+        screen.getByRole('img', {
+          name: kind === 'profile' ? 'Message In TPS time series' : '自定义查询 
time series',
+        }),
+      ).toBeInTheDocument();
+      expect(drawer.querySelectorAll('.ant-list-item')).toHaveLength(0);
+      
expect(localStorage.getItem(METRICS_QUERY_HISTORY_STORAGE_KEY)).toBeNull();
+
+      // New queries after clearing remain eligible for history.
+      await user.click(within(drawer).getByRole('button', { name: 'Close' }));
+      await user.click(screen.getByRole('button', { name: '刷新全部面板' }));
+      await waitFor(() => {
+        const stored = 
JSON.parse(localStorage.getItem(METRICS_QUERY_HISTORY_STORAGE_KEY)!);
+        expect(stored).toHaveLength(kind === 'profile' ? 1 : 2);
+      });
+    },
+  );
+
+  it('keepsPostClearHistoryWhenAnOlderIndependentProfileQueryCompletesTest', 
async () => {
+    const user = userEvent.setup();
+    renderWithProviders(<MetricsExplorer />);
+    await screen.findByRole('img', { name: 'Message In TPS time series' });
+    const pending = createDeferred<MetricData>();
+    vi.mocked(queryMetrics).mockReturnValueOnce(pending.promise);
+    await user.click(screen.getByRole('button', { name: '刷新全部面板' }));
+    await user.click(screen.getByRole('button', { name: '查询历史' }));
+    const drawer = (await screen.findByText('指标查询历史')).closest(
+      '[role="dialog"]',
+    ) as HTMLElement;
+    await user.click(within(drawer).getByRole('button', { name: '清空历史' }));
+    await user.click(within(drawer).getByRole('button', { name: 'Close' }));
+
+    await user.type(screen.getByLabelText('自定义查询'), 
'sum(rocketmq_topic_number)');
+    await user.click(screen.getByRole('button', { name: '查询' }));
+    await screen.findByRole('img', { name: '自定义查询 time series' });
+    const stored = localStorage.getItem(METRICS_QUERY_HISTORY_STORAGE_KEY);
+    expect(JSON.parse(stored!)).toHaveLength(1);
+
+    await act(async () => pending.resolve(metricData));
+    expect(screen.getByRole('img', { name: 'Message In TPS time series' 
})).toBeInTheDocument();
+    
expect(localStorage.getItem(METRICS_QUERY_HISTORY_STORAGE_KEY)).toBe(stored);
+    await user.click(screen.getByRole('button', { name: '查询历史' }));
+    const reopened = (await screen.findByText('指标查询历史')).closest(
+      '[role="dialog"]',
+    ) as HTMLElement;
+    expect(reopened.querySelectorAll('.ant-list-item')).toHaveLength(1);
+    
expect(reopened.querySelector('.ant-list-item')).toHaveTextContent('自定义查询');
+  });
+
   it('restores profile, range, and source from query history', async () => {
     const user = userEvent.setup();
     localStorage.setItem(METRICS_QUERY_HISTORY_STORAGE_KEY, 
JSON.stringify([createHistoryEntry()]));

Reply via email to