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 bd7718137 fix(metrics): re-run the custom query when the range changes
(#4901)
bd7718137 is described below
commit bd77181379aee9348f46518d86017070f3a639da
Author: 烤化の初雪 <[email protected]>
AuthorDate: Thu Sep 24 18:21:11 2026 +0800
fix(metrics): re-run the custom query when the range changes (#4901)
test(metrics): pin that the custom re-run leaves the panel flow alone
The fix re-runs the committed custom query on a range change, which now sits
beside the profile panels' own loadAll — the cross-flow interaction tracked
by
issue #3304 and fixed by #3299. The regression test also asserts that both
flows publish their result after the range change, with the two payloads
distinguished by their values so a stranded panel fails the assertion.
Checked that the assertion has teeth: making runCustomQuery bump
panelRequestIdRef as well (the shared-generation freeze of #3304) makes it
fail
with "Unable to find an element with the text: 77 messages/s", while the
fix as
written keeps the two request generations separate.
fix(metrics): re-run the custom query when the range changes
The range control scopes the whole explorer, and every other path that
changes
the window re-runs the committed custom query: the refresh button does it
(MetricsExplorer.tsx:1354-1357) and an instance switch does it (:878-880).
handleRangeChange only called loadAll, so after committing a custom
expression
and picking another range the profile panels moved to the new window while
the
custom panel kept the samples, the query-window popover and the CSV
metadata of
the previous one — the visible range control disagreed with the panel under
it.
Re-run the committed custom query with the next range, mirroring the refresh
handler. handleRangeChange moves below runCustomQuery so it can call it.
Regression test: a committed custom query is re-issued with the 6h window
(start 1799978400, step 2m) when the range changes, failing before the
change
with "expected [...] to have a length of 2 but got 1".
---
web/src/components/MetricsExplorer.tsx | 19 ++++---
.../components/__tests__/MetricsExplorer.test.tsx | 66 ++++++++++++++++++++++
2 files changed, 78 insertions(+), 7 deletions(-)
diff --git a/web/src/components/MetricsExplorer.tsx
b/web/src/components/MetricsExplorer.tsx
index cb1c0fa50..b4c37865a 100644
--- a/web/src/components/MetricsExplorer.tsx
+++ b/web/src/components/MetricsExplorer.tsx
@@ -723,13 +723,6 @@ const MetricsExplorer = ({ instanceId }:
MetricsExplorerProps) => {
void loadAll(nextProfile, selectedRange);
};
- const handleRangeChange = (nextRangeId: RangeOption['value']) => {
- const nextRange =
- RANGE_OPTIONS.find((range) => range.value === nextRangeId) ??
RANGE_OPTIONS[0];
- setRangeId(nextRangeId);
- void loadAll(selectedProfile, nextRange);
- };
-
const runCustomQuery = useCallback(
async (promql: string, range: RangeOption) => {
const trimmed = promql.trim();
@@ -795,6 +788,18 @@ const MetricsExplorer = ({ instanceId }:
MetricsExplorerProps) => {
[copy.customTitle, instanceId, queryErrorFallback, runQuery],
);
+ const handleRangeChange = (nextRangeId: RangeOption['value']) => {
+ const nextRange =
+ RANGE_OPTIONS.find((range) => range.value === nextRangeId) ??
RANGE_OPTIONS[0];
+ setRangeId(nextRangeId);
+ void loadAll(selectedProfile, nextRange);
+ // The range control scopes the whole explorer, so a committed custom
query follows the new
+ // window the same way the refresh button and an instance switch already
make it follow.
+ if (appliedCustomPromql) {
+ void runCustomQuery(appliedCustomPromql, nextRange);
+ }
+ };
+
const activateDataSource = (
nextKey: string,
credentials?: AuthFormValues,
diff --git a/web/src/components/__tests__/MetricsExplorer.test.tsx
b/web/src/components/__tests__/MetricsExplorer.test.tsx
index bf63839d7..c353f9c57 100644
--- a/web/src/components/__tests__/MetricsExplorer.test.tsx
+++ b/web/src/components/__tests__/MetricsExplorer.test.tsx
@@ -504,6 +504,72 @@ describe('MetricsExplorer', () => {
expect(screen.getByText('cluster=prod /
query=custom')).toBeInTheDocument();
});
+ it('re-runs the committed custom query when the range changes', async () => {
+ const user = userEvent.setup();
+ renderWithProviders(<MetricsExplorer />);
+ await screen.findByRole('img', { name: 'Message In TPS time series' });
+
+ await user.type(screen.getByLabelText('自定义查询'),
'sum(rocketmq_topic_number)');
+ await user.click(screen.getByRole('button', { name: '查询' }));
+ await waitFor(() =>
+ expect(queryMetrics).toHaveBeenCalledWith({
+ metric: 'sum(rocketmq_topic_number)',
+ start: 1_799_996_400,
+ end: 1_800_000_000,
+ step: '30s',
+ }),
+ );
+
+ // The range control governs the whole explorer: the profile panels
re-query through
+ // loadAll, so the custom panel has to follow the new window as well
instead of
+ // keeping the samples its previous window produced.
+ const refreshedProfileData = {
+ ...metricData,
+ series: [
+ {
+ ...metricData.series[0],
+ values: [{ timestamp: 1_800_000_000, value: '77' }],
+ },
+ ],
+ };
+ const refreshedCustomData = {
+ ...metricData,
+ series: [
+ {
+ ...metricData.series[0],
+ labels: { cluster: 'prod', query: 'custom' },
+ values: [{ timestamp: 1_800_000_000, value: '9' }],
+ },
+ ],
+ };
+ vi.mocked(queryMetrics).mockImplementation((query) =>
+ Promise.resolve(
+ query.metric === 'sum(rocketmq_topic_number)' ? refreshedCustomData :
refreshedProfileData,
+ ),
+ );
+
+ await user.click(screen.getByText('6h'));
+
+ const customCalls = () =>
+ vi
+ .mocked(queryMetrics)
+ .mock.calls.filter((call) => call[0].metric ===
'sum(rocketmq_topic_number)');
+ await waitFor(() => expect(customCalls()).toHaveLength(2));
+ const rerun = customCalls()[customCalls().length - 1];
+ expect(rerun[0]).toEqual({
+ metric: 'sum(rocketmq_topic_number)',
+ start: 1_799_978_400,
+ end: 1_800_000_000,
+ step: '2m',
+ });
+
+ // The custom panel re-runs beside the profile panels, so the added call
must leave the
+ // other flow's request generation alone: both still publish their own
result. (The
+ // cross-flow freeze of issue #3304, fixed by #3299, was the opposite
behaviour.)
+ expect(await screen.findByText('77 messages/s')).toBeInTheDocument();
+ expect(screen.getByText('cluster=prod /
query=custom')).toBeInTheDocument();
+ });
+
it('queries the first metric when the version profile changes', async () => {
const user = userEvent.setup();
renderWithProviders(<MetricsExplorer />);