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 00905bd7f fix(metrics): keep a language switch out of the query 
identity (#4888)
00905bd7f is described below

commit 00905bd7fc60a301a1f42fa45624a67c0317e971
Author: 烤化の初雪 <[email protected]>
AuthorDate: Thu Oct 1 17:25:05 2026 +0800

    fix(metrics): keep a language switch out of the query identity (#4888)
    
    `MetricsExplorer` builds its query callbacks from the display text: the 
labels
    `copy.defaultDataSource` / `copy.customTitle` and the `queryErrorFallback`
    message are values of the current language. They sat in the dependency 
arrays of
    `runQuery`/`loadAll`/`runCustomQuery`, so switching the display language in 
the
    top bar rebuilt all three callbacks and re-ran the profiles effect, which
    
    - refetched the metric profile list and re-queried every profile panel, and
    - bumped `customRequestIdRef`, discarding a custom query that was still in
      flight.
    
    The custom flow is only restarted by the instance-transition and data-source
    fallback paths, so the discarded request had no replacement: the custom 
panel
    stayed on its spinner, the Run button stayed busy (antd suppresses clicks 
while
    a Button is loading) and `anyLoading` kept the refresh action disabled 
until the
    page was reloaded.
    
    The labels now live in a ref that the callbacks read, which keeps them
    language-independent while still rendering the live text. The profiles 
effect
    re-runs only for the query context it was written for (the instance), so a
    language switch neither refetches the panels nor abandons in-flight work.
    
    Co-authored-by: unbridled-41 
<[email protected]>
---
 web/src/components/MetricsExplorer.tsx             | 38 +++++++---
 .../components/__tests__/MetricsExplorer.test.tsx  | 81 +++++++++++++++++++++-
 2 files changed, 109 insertions(+), 10 deletions(-)

diff --git a/web/src/components/MetricsExplorer.tsx 
b/web/src/components/MetricsExplorer.tsx
index 67027d8a2..a40cc41f8 100644
--- a/web/src/components/MetricsExplorer.tsx
+++ b/web/src/components/MetricsExplorer.tsx
@@ -560,6 +560,26 @@ const MetricsExplorer = ({ instanceId }: 
MetricsExplorerProps) => {
   useEffect(() => {
     rangeIdRef.current = rangeId;
   }, [rangeId]);
+  // `copy` and `queryErrorFallback` are display text that is rebuilt whenever 
the
+  // language changes, so the query callbacks below read the labels they need 
through a
+  // ref instead of capturing them. Depending on the language made a switch 
look like a
+  // change of query context: the profiles effect re-ran, refetched the 
profile list and
+  // every panel, and discarded in-flight custom queries whose replacement is 
only
+  // started by the instance-transition path, leaving the custom panel on its 
spinner
+  // for good. Mirroring the labels (strings, so the effect stays quiet 
between language
+  // switches) keeps both callbacks language-independent.
+  const queryLabelsRef = useRef({
+    defaultDataSource: copy.defaultDataSource,
+    customTitle: copy.customTitle,
+    queryErrorFallback,
+  });
+  useEffect(() => {
+    queryLabelsRef.current = {
+      defaultDataSource: copy.defaultDataSource,
+      customTitle: copy.customTitle,
+      queryErrorFallback,
+    };
+  }, [copy.defaultDataSource, copy.customTitle, queryErrorFallback]);
 
   const selectedProfile = useMemo(
     () => profiles.find((profile) => profile.id === profileId),
@@ -617,13 +637,13 @@ const MetricsExplorer = ({ instanceId }: 
MetricsExplorerProps) => {
         dataSourceKey: currentDataSourceKey,
         dataSourceName:
           dataSourceNamesRef.current.get(currentDataSourceKey) ??
-          (currentDataSourceKey || copy.defaultDataSource),
+          (currentDataSourceKey || queryLabelsRef.current.defaultDataSource),
         start: query.start,
         end: query.end,
         queriedAt,
       };
     },
-    [copy.defaultDataSource, instanceId],
+    [instanceId],
   );
 
   const loadAll = useCallback(
@@ -683,7 +703,7 @@ const MetricsExplorer = ({ instanceId }: 
MetricsExplorerProps) => {
                 ...previous,
                 [metric.semanticMetric]: {
                   loading: false,
-                  error: getQueryErrorMessage(error, queryErrorFallback),
+                  error: getQueryErrorMessage(error, 
queryLabelsRef.current.queryErrorFallback),
                 },
               }));
             }
@@ -691,7 +711,7 @@ const MetricsExplorer = ({ instanceId }: 
MetricsExplorerProps) => {
         }),
       );
     },
-    [instanceId, queryErrorFallback, runQuery],
+    [instanceId, runQuery],
   );
 
   useEffect(() => {
@@ -746,7 +766,7 @@ const MetricsExplorer = ({ instanceId }: 
MetricsExplorerProps) => {
         if (currentRequest === customRequestIdRef.current) {
           const metric: MetricMapping = {
             semanticMetric: CUSTOM_HISTORY_METRIC_ID,
-            name: copy.customTitle,
+            name: queryLabelsRef.current.customTitle,
             unit: '',
             prometheusMetric: '',
             promql: trimmed,
@@ -755,7 +775,7 @@ const MetricsExplorer = ({ instanceId }: 
MetricsExplorerProps) => {
           const summary = summarizeMetricData(execution.data);
           const query: PanelQueryMeta = {
             profileId: CUSTOM_HISTORY_PROFILE_ID,
-            profileName: copy.customTitle,
+            profileName: queryLabelsRef.current.customTitle,
             metric,
             range,
             dataSourceKey: execution.dataSourceKey,
@@ -767,7 +787,7 @@ const MetricsExplorer = ({ instanceId }: 
MetricsExplorerProps) => {
           };
           const historyEntry = createMetricsQueryHistoryEntry({
             profileId: CUSTOM_HISTORY_PROFILE_ID,
-            profileName: copy.customTitle,
+            profileName: queryLabelsRef.current.customTitle,
             metric,
             rangeId: range.value,
             rangeLabel: range.label,
@@ -791,12 +811,12 @@ const MetricsExplorer = ({ instanceId }: 
MetricsExplorerProps) => {
         if (currentRequest === customRequestIdRef.current) {
           setCustomPanel({
             loading: false,
-            error: getQueryErrorMessage(error, queryErrorFallback),
+            error: getQueryErrorMessage(error, 
queryLabelsRef.current.queryErrorFallback),
           });
         }
       }
     },
-    [copy.customTitle, instanceId, queryErrorFallback, runQuery],
+    [instanceId, runQuery],
   );
 
   const handleRangeChange = (nextRangeId: RangeOption['value']) => {
diff --git a/web/src/components/__tests__/MetricsExplorer.test.tsx 
b/web/src/components/__tests__/MetricsExplorer.test.tsx
index 53b01fa7f..97a1425b7 100644
--- a/web/src/components/__tests__/MetricsExplorer.test.tsx
+++ b/web/src/components/__tests__/MetricsExplorer.test.tsx
@@ -23,7 +23,7 @@ import { afterEach, beforeAll, beforeEach, describe, expect, 
it, vi } from 'vite
 
 import { listDataSources } from '../../api/settings';
 import { listMetricProfiles, queryByDataSource, queryMetrics } from 
'../../api/metrics';
-import { LangProvider } from '../../i18n/LangContext';
+import { LangProvider, useLang } from '../../i18n/LangContext';
 import { downloadCsv } from '../../utils/download';
 import {
   METRICS_QUERY_HISTORY_STORAGE_KEY,
@@ -187,6 +187,15 @@ const renderWithProviders = (ui: React.ReactElement) =>
     </App>,
   );
 
+const LanguageSwitch = () => {
+  const { setLang } = useLang();
+  return (
+    <button type="button" onClick={() => setLang('en')}>
+      switch-language
+    </button>
+  );
+};
+
 const createDeferred = <T,>() => {
   let resolve!: (value: T) => void;
   const promise = new Promise<T>((resolvePromise) => {
@@ -456,6 +465,76 @@ describe('MetricsExplorer', () => {
     );
   });
 
+  it('keeps the pending custom query when the display language changes', async 
() => {
+    const user = userEvent.setup();
+    const customQuery = createDeferred<typeof metricData>();
+    vi.mocked(queryMetrics).mockImplementation((query) =>
+      query.metric === 'sum(rocketmq_topic_number)'
+        ? customQuery.promise
+        : Promise.resolve(metricData),
+    );
+
+    render(
+      <App>
+        <LangProvider>
+          <LanguageSwitch />
+          <MetricsExplorer />
+        </LangProvider>
+      </App>,
+    );
+    await screen.findByText('42 messages/s');
+
+    await user.type(screen.getByLabelText('自定义查询'), 
'sum(rocketmq_topic_number)');
+    await user.click(screen.getByRole('button', { name: '查询' }));
+    await waitFor(() =>
+      expect(
+        vi
+          .mocked(queryMetrics)
+          .mock.calls.filter((call) => call[0].metric === 
'sum(rocketmq_topic_number)'),
+      ).toHaveLength(1),
+    );
+
+    // The display language is not part of the query context: switching it 
must neither
+    // refetch the profile panels nor abandon the custom query that is still 
running.
+    await user.click(screen.getByRole('button', { name: 'switch-language' }));
+
+    expect(listMetricProfiles).toHaveBeenCalledTimes(1);
+    expect(
+      vi
+        .mocked(queryMetrics)
+        .mock.calls.filter(
+          (call) =>
+            call[0].metric === 'sum(rate(rocketmq_messages_in_total[1m])) by 
(cluster, node_id)',
+        ),
+    ).toHaveLength(1);
+
+    customQuery.resolve({
+      ...metricData,
+      series: [
+        {
+          ...metricData.series[0],
+          labels: { cluster: 'prod', query: 'custom' },
+          values: [{ timestamp: 1_800_000_000, value: '9' }],
+        },
+      ],
+    });
+
+    // Before the fix the language change discarded this result and nothing 
restarted the
+    // custom flow, so the panel stayed on its spinner until the page was 
reloaded.
+    expect(await screen.findByText('cluster=prod / 
query=custom')).toBeInTheDocument();
+
+    // antd suppresses clicks while a Button is loading, so a second 
submission proves the
+    // custom panel released the stuck loading state.
+    await user.click(screen.getByRole('button', { name: 'Run' }));
+    await waitFor(() =>
+      expect(
+        vi
+          .mocked(queryMetrics)
+          .mock.calls.filter((call) => call[0].metric === 
'sum(rocketmq_topic_number)'),
+      ).toHaveLength(2),
+    );
+  });
+
   it('refreshes profile panels and the custom query independently', async () 
=> {
     const user = userEvent.setup();
     renderWithProviders(<MetricsExplorer />);

Reply via email to