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 />);