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 5b98f944a fix(message): paginate against the committed query (#4005)
5b98f944a is described below
commit 5b98f944a3b1395639c3eaddc8760fa3b5e1d74a
Author: 烤化の初雪 <[email protected]>
AuthorDate: Mon Sep 7 18:39:12 2026 +0800
fix(message): paginate against the committed query (#4005)
The results table rebuilt the query from the live form inputs on every
pagination click, so editing the topic/date range/keys after running a
query and then clicking page 2 silently executed a different query:
page 1 showed the committed results while page 2 was fetched with the
uncommitted inputs, producing a mixed result list.
Snapshot the executed query (mode and normalized params) whenever a
query actually runs — including history replays — and paginate against
that snapshot instead of the live inputs.
Co-authored-by: unbridled-41
<[email protected]>
---
.../pages/instance/__tests__/MessagePage.test.tsx | 40 ++++++++++++-
web/src/pages/instance/message.tsx | 66 +++++++++-------------
2 files changed, 66 insertions(+), 40 deletions(-)
diff --git a/web/src/pages/instance/__tests__/MessagePage.test.tsx
b/web/src/pages/instance/__tests__/MessagePage.test.tsx
index c669279d7..f2dadaad5 100644
--- a/web/src/pages/instance/__tests__/MessagePage.test.tsx
+++ b/web/src/pages/instance/__tests__/MessagePage.test.tsx
@@ -16,7 +16,7 @@
*/
import { App } from 'antd';
-import { render, screen, waitFor } from '@testing-library/react';
+import { fireEvent, render, screen, waitFor } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import type React from 'react';
import { MemoryRouter } from 'react-router-dom';
@@ -318,4 +318,42 @@ describe('Message page query history', () => {
expect(screen.queryByText('order-create')).not.toBeInTheDocument();
});
+
+ it('paginates against the committed query instead of live form inputs',
async () => {
+ messageServiceMocks.queryMessages.mockResolvedValue(
+ Array.from({ length: 60 }, (_, index) => createMessage(`m-${index}`)),
+ );
+ const user = userEvent.setup();
+ renderWithProviders(<MessagePage />);
+
+ await user.click(lastElement(screen.getAllByRole('combobox')));
+ await user.click(lastElement(await screen.findAllByText('order-create')));
+ await user.click(screen.getByRole('button', { name: /^search查询$/ }));
+ await waitFor(() => {
+ expect(messageServiceMocks.queryMessages).toHaveBeenLastCalledWith(
+ expect.objectContaining({ topic: 'order-create' }),
+ );
+ });
+
+ // Edit the form without re-running the query.
+ await user.click(screen.getAllByRole('combobox')[1]);
+ const visibleOption = (await screen.findAllByText('payment-callback'))
+ .map((element) => element.closest('.ant-select-item-option'))
+ .find(
+ (element): element is HTMLElement =>
+ element instanceof HTMLElement &&
+
element.closest('.ant-select-dropdown:not(.ant-select-dropdown-hidden)') !==
null,
+ );
+ if (!visibleOption) throw new Error('Visible topic option not found');
+ fireEvent.click(visibleOption);
+
+ const secondPage = document.querySelector('.ant-pagination-item-2') as
HTMLElement | null;
+ if (!secondPage) throw new Error('Pagination page 2 not found');
+ await user.click(secondPage);
+ await waitFor(() => {
+ expect(messageServiceMocks.queryMessages).toHaveBeenLastCalledWith(
+ expect.objectContaining({ topic: 'order-create' }),
+ );
+ });
+ });
});
diff --git a/web/src/pages/instance/message.tsx
b/web/src/pages/instance/message.tsx
index c2028652c..45fab7e49 100644
--- a/web/src/pages/instance/message.tsx
+++ b/web/src/pages/instance/message.tsx
@@ -59,7 +59,6 @@ import {
QueueBrowserResults,
} from '../../components/QueueBrowser';
import type { MessageQueryHistory, TraceQueryHistory } from
'../../api/messageHistory';
-import { getMessageQueryResults } from '../../api/messageHistory';
import { useLang } from '../../i18n/LangContext';
import type { MessageQuery, MessageRecord, TraceRecord } from
'../../api/message';
import {
@@ -410,6 +409,9 @@ const MessagePageContent = ({
const [directConsumeClientId, setDirectConsumeClientId] = useState('');
const [directConsumeSubmitting, setDirectConsumeSubmitting] =
useState(false);
const queryGenerationRef = useRef(0);
+ // The query whose results the table currently shows. Pagination must re-run
this
+ // committed query, not whatever the form inputs hold at the moment a page
is clicked.
+ const committedQueryRef = useRef<{ mode: QueryMode; params: MessageQuery } |
null>(null);
const traceGenerationRef = useRef(0);
const traceCacheRef = useRef(new Map<string, Promise<TraceRecord | null>>());
const traceDiagnostics = useMemo(() => analyzeMessageTrace(traceData),
[traceData]);
@@ -449,6 +451,7 @@ const MessagePageContent = ({
setResultMayBeTruncated(false);
setQueryError(null);
setQueryLoading(false);
+ committedQueryRef.current = null;
};
const handleReset = () => {
@@ -497,6 +500,9 @@ const MessagePageContent = ({
pageSize,
});
if (queryGenerationRef.current !== requestGeneration) return;
+ // Commit only the query whose results are actually on screen: a failed
or superseded
+ // request must not become the query that pagination re-runs.
+ committedQueryRef.current = { mode, params: normalizedParams };
setMessages(result.items);
setMessageTotal(result.total);
setMessagePage(result.page);
@@ -530,42 +536,21 @@ const MessagePageContent = ({
setDateRange([dayjs(record.startTime), dayjs(record.endTime)]);
}
setHistoryDrawerOpen(false);
- const requestGeneration = queryGenerationRef.current + 1;
- queryGenerationRef.current = requestGeneration;
- setQueryLoading(true);
- setQueryError(null);
- try {
- const results = await getMessageQueryResults(record.id);
- if (queryGenerationRef.current !== requestGeneration) return;
- const mapped: MessageRecord[] = results.map((r) => ({
- msgId: r.msgId,
- topic: r.topic,
- tag: r.tag || null,
- key: r.key || null,
- brokerName: r.brokerName || null,
- queueId: r.queueId,
- queueOffset: r.queueOffset,
- body: '',
- storeTime: r.storeTime,
- bornHost: r.bornHost,
- storeHost: r.storeHost,
- properties: {},
- size: r.size,
- }));
- setMessages(mapped);
- setMessageTotal(mapped.length);
- setMessagePage(1);
- setResultMayBeTruncated(false);
- message.success(`已加载历史查询结果,共 ${mapped.length} 条`);
- } catch (error) {
- if (queryGenerationRef.current === requestGeneration) {
- setQueryError(getErrorMessage(error, '加载历史结果失败'));
- }
- } finally {
- if (queryGenerationRef.current === requestGeneration) {
- setQueryLoading(false);
- }
- }
+ // Re-run the historical query through the same live path so it is
normalized and committed
+ // exactly like a normal search: the displayed page and any later
pagination then share one
+ // query. Loading the archived snapshot here instead would splice snapshot
page 1 with a live
+ // page 2 on the next pagination click — the very mix this fix removes.
+ const params: MessageQuery =
+ mode === 'topic'
+ ? {
+ topic: record.topic,
+ ...(record.startTime !== undefined ? { startTime: record.startTime
} : {}),
+ ...(record.endTime !== undefined ? { endTime: record.endTime } :
{}),
+ }
+ : mode === 'key'
+ ? { topic: record.topic, key: record.messageKey || undefined }
+ : { topic: record.topic, msgId: record.msgId || undefined };
+ await executeQuery(mode, params);
};
const replayTraceRecord = (record: TraceQueryHistory) => {
@@ -1213,8 +1198,11 @@ const MessagePageContent = ({
total: messageTotal,
showSizeChanger: true,
showTotal: (total) => `共 ${total} 条消息`,
- onChange: (page, pageSize) =>
- void executeQuery(queryMode, currentQueryParams, page,
pageSize),
+ onChange: (page, pageSize) => {
+ const committed = committedQueryRef.current;
+ if (!committed) return;
+ void executeQuery(committed.mode, committed.params, page,
pageSize);
+ },
}}
size="small"
scroll={{ x: tableScrollX(columns) }}