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()]));