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 60ec70b1 fix: validate Message Explorer query inputs by mode (#1501)
60ec70b1 is described below
commit 60ec70b1a228c7df55e7e5bf7a47238f91083b3b
Author: majialong <[email protected]>
AuthorDate: Tue Aug 11 20:18:10 2026 +0800
fix: validate Message Explorer query inputs by mode (#1501)
---
.../pages/instance/__tests__/MessagePage.test.tsx | 105 ++++++++++++++++++++-
.../__tests__/MessagePageAsyncState.test.tsx | 39 ++++++--
web/src/pages/instance/message.tsx | 83 ++++++++++++----
3 files changed, 196 insertions(+), 31 deletions(-)
diff --git a/web/src/pages/instance/__tests__/MessagePage.test.tsx
b/web/src/pages/instance/__tests__/MessagePage.test.tsx
index cb5d1c8b..7762e3e2 100644
--- a/web/src/pages/instance/__tests__/MessagePage.test.tsx
+++ b/web/src/pages/instance/__tests__/MessagePage.test.tsx
@@ -112,6 +112,94 @@ describe('Message page query history', () => {
vi.restoreAllMocks();
});
+ it('requires the active query mode fields and trims submitted identifiers',
async () => {
+ const user = userEvent.setup();
+ renderWithProviders(<MessagePage />);
+ const queryButton = screen.getByRole('button', { name: /^search查询$/ });
+
+ expect(queryButton).toBeDisabled();
+ expect(queryButton).toHaveAttribute('title', '请选择 Topic');
+
+ await user.click(lastElement(screen.getAllByRole('combobox')));
+ await user.click(lastElement(await screen.findAllByText('order-create')));
+ expect(queryButton).toBeEnabled();
+ expect(queryButton).not.toHaveAttribute('title');
+
+ await user.click(screen.getByText('按 Message Key'));
+ expect(queryButton).toBeDisabled();
+ expect(queryButton).toHaveAttribute('title', '请输入 Message Key');
+
+ const keyInput = screen.getByPlaceholderText('输入 Message Key');
+ await user.type(keyInput, ' ');
+ expect(queryButton).toBeDisabled();
+ expect(messageServiceMocks.queryMessages).not.toHaveBeenCalled();
+
+ await user.clear(keyInput);
+ await user.type(keyInput, ' ORDER-001 ');
+ expect(queryButton).toBeEnabled();
+ await user.click(queryButton);
+ await waitFor(() => {
+ expect(messageServiceMocks.queryMessages).toHaveBeenLastCalledWith({
+ topic: 'order-create',
+ key: 'ORDER-001',
+ instanceId: 'instance-a',
+ });
+ });
+
+ await user.click(screen.getByText('按 Message ID'));
+ expect(queryButton).toBeDisabled();
+ expect(queryButton).toHaveAttribute('title', '请输入 Message ID');
+
+ const messageIdInput = screen.getByPlaceholderText('输入 Message ID');
+ await user.type(messageIdInput, ' ');
+ expect(queryButton).toBeDisabled();
+
+ await user.clear(messageIdInput);
+ await user.type(messageIdInput, ' MID-001 ');
+ expect(queryButton).toBeEnabled();
+ await user.click(queryButton);
+ await waitFor(() => {
+ expect(messageServiceMocks.queryMessages).toHaveBeenLastCalledWith({
+ topic: 'order-create',
+ msgId: 'MID-001',
+ instanceId: 'instance-a',
+ });
+ });
+ });
+
+ it('requires a topic even when a key or message ID is present', async () => {
+ const user = userEvent.setup();
+ renderWithProviders(<MessagePage />);
+ const queryButton = screen.getByRole('button', { name: /^search查询$/ });
+
+ await user.click(screen.getByText('按 Message Key'));
+ await user.type(screen.getByPlaceholderText('输入 Message Key'),
'ORDER-001');
+ expect(queryButton).toBeDisabled();
+ expect(queryButton).toHaveAttribute('title', '请选择 Topic');
+
+ await user.click(screen.getByText('按 Message ID'));
+ await user.type(screen.getByPlaceholderText('输入 Message ID'), 'MID-001');
+ expect(queryButton).toBeDisabled();
+ expect(queryButton).toHaveAttribute('title', '请选择 Topic');
+ expect(messageServiceMocks.queryMessages).not.toHaveBeenCalled();
+ });
+
+ it('ignores stored queries that are missing fields required by their mode',
() => {
+ localStorage.setItem(
+ QUERY_HISTORY_STORAGE_KEY,
+ JSON.stringify([
+ { mode: 'topic', params: {} },
+ { mode: 'key', params: { topic: 'order-create', key: ' ' } },
+ { mode: 'msgid', params: { topic: 'order-create', msgId: ' ' } },
+ ]),
+ );
+
+ renderWithProviders(<MessagePage />);
+
+ expect(screen.getByRole('button', { name: /最近查询/ })).toBeDisabled();
+ expect(messageServiceMocks.queryMessages).not.toHaveBeenCalled();
+ });
+
it('persists successful queries for replay and allows clearing the history',
async () => {
const user = userEvent.setup();
const firstView = renderWithProviders(<MessagePage />);
@@ -233,8 +321,13 @@ describe('Message page query history', () => {
tag: 'vip',
startTime: 1_700_000_000_000,
endTime: 1_700_003_600_000,
+ msgId: 'STALE-MESSAGE-ID',
+ };
+ const keyParams = {
+ topic: 'payment-callback',
+ key: 'ORDER-001',
+ msgId: 'STALE-MESSAGE-ID',
};
- const keyParams = { topic: 'payment-callback', key: 'ORDER-001' };
localStorage.setItem(
QUERY_HISTORY_STORAGE_KEY,
JSON.stringify([
@@ -248,7 +341,10 @@ describe('Message page query history', () => {
await user.click(await screen.findByText('Topic: order-create'));
await waitFor(() => {
expect(messageServiceMocks.queryMessages).toHaveBeenLastCalledWith({
- ...topicParams,
+ topic: 'order-create',
+ tag: 'vip',
+ startTime: 1_700_000_000_000,
+ endTime: 1_700_003_600_000,
instanceId: 'instance-a',
});
});
@@ -257,7 +353,8 @@ describe('Message page query history', () => {
await user.click(await screen.findByText('Key: ORDER-001 · Topic:
payment-callback'));
await waitFor(() => {
expect(messageServiceMocks.queryMessages).toHaveBeenLastCalledWith({
- ...keyParams,
+ topic: 'payment-callback',
+ key: 'ORDER-001',
instanceId: 'instance-a',
});
expect(screen.getByPlaceholderText('输入 Message
Key')).toHaveValue('ORDER-001');
@@ -312,6 +409,8 @@ describe('Message page query history', () => {
renderWithProviders(<MessagePage />);
await user.click(screen.getByText('按 Message ID'));
+ await user.click(lastElement(screen.getAllByRole('combobox')));
+ await user.click(lastElement(await screen.findAllByText('order-create')));
await user.type(screen.getByPlaceholderText('输入 Message ID'), 'MID');
await user.click(screen.getByRole('button', { name: /^search查询$/ }));
diff --git a/web/src/pages/instance/__tests__/MessagePageAsyncState.test.tsx
b/web/src/pages/instance/__tests__/MessagePageAsyncState.test.tsx
index 21d4c3d4..e543ea6b 100644
--- a/web/src/pages/instance/__tests__/MessagePageAsyncState.test.tsx
+++ b/web/src/pages/instance/__tests__/MessagePageAsyncState.test.tsx
@@ -39,7 +39,7 @@ vi.mock('../../../services/instanceService', () => ({
listInstances: vi.fn().mockResolvedValue([]),
}));
vi.mock('../../../services/topicService', () => ({
- listTopics: vi.fn().mockResolvedValue([]),
+ listTopics: vi.fn().mockResolvedValue([{ name: 'topic-a' }]),
}));
beforeAll(() => {
@@ -108,6 +108,13 @@ const MessagePageWithProviders = () => (
const renderPage = () => render(<MessagePageWithProviders />);
+const selectTopic = async (user: ReturnType<typeof userEvent.setup>) => {
+ const topicSelects = screen.getAllByRole('combobox');
+ await user.click(topicSelects[topicSelects.length - 1]!);
+ const topicOptions = await screen.findAllByText('topic-a');
+ await user.click(topicOptions[topicOptions.length - 1]!);
+};
+
describe('MessagePage async request ownership', () => {
beforeEach(() => {
vi.clearAllMocks();
@@ -129,6 +136,7 @@ describe('MessagePage async request ownership', () => {
serviceMocks.queryMessages.mockReturnValue(query.promise);
const user = userEvent.setup();
renderPage();
+ await selectTopic(user);
await user.click(screen.getByRole('button', { name: /^search查询$/ }));
await waitFor(() =>
expect(serviceMocks.queryMessages).toHaveBeenCalledTimes(1));
@@ -143,25 +151,33 @@ describe('MessagePage async request ownership', () => {
it('clears query results and message details when the selected instance
changes', async () => {
serviceMocks.queryMessages.mockResolvedValue([createMessage('message-from-instance-a')]);
+ const selectInstance = vi.fn();
+ instanceFilterMocks.useInstanceFilter.mockReturnValue({
+ selectedInstanceId: 'instance-a',
+ selectInstance,
+ instanceOptions: [
+ { value: 'instance-a', label: 'Instance A' },
+ { value: 'instance-b', label: 'Instance B' },
+ ],
+ });
const user = userEvent.setup();
- const page = renderPage();
+ renderPage();
+ await selectTopic(user);
await user.click(screen.getByRole('button', { name: /^search查询$/ }));
const row = await screen.findByRole('row', { name:
/message-from-instance-a/ });
await user.click(within(row).getByRole('button', { name: /详情/ }));
expect(await screen.findByRole('dialog', { name: '消息详情'
})).toBeInTheDocument();
- instanceFilterMocks.useInstanceFilter.mockReturnValue({
- selectedInstanceId: 'instance-b',
- selectInstance: vi.fn(),
- instanceOptions: [{ value: 'instance-b', label: 'Instance B' }],
- });
- page.rerender(<MessagePageWithProviders />);
+ await user.click(screen.getAllByRole('combobox')[0]!);
+ const instanceOptions = await screen.findAllByText('Instance B');
+ await user.click(instanceOptions[instanceOptions.length - 1]!);
await waitFor(() => {
expect(screen.queryByText('message-from-instance-a')).not.toBeInTheDocument();
expect(screen.queryByRole('dialog', { name: '消息详情'
})).not.toBeInTheDocument();
});
+ expect(selectInstance).toHaveBeenCalledWith('instance-b');
});
it('surfaces unavailable message provider errors from query requests', async
() => {
serviceMocks.queryMessages.mockRejectedValue(
@@ -169,6 +185,7 @@ describe('MessagePage async request ownership', () => {
);
const user = userEvent.setup();
renderPage();
+ await selectTopic(user);
await user.click(screen.getByRole('button', { name: /^search查询$/ }));
@@ -183,6 +200,7 @@ describe('MessagePage async request ownership', () => {
);
const user = userEvent.setup();
renderPage();
+ await selectTopic(user);
await user.click(screen.getByRole('button', { name: /^search查询$/ }));
const row = await screen.findByRole('row', { name: /message-a/ });
@@ -198,6 +216,7 @@ describe('MessagePage async request ownership', () => {
serviceMocks.queryMessages.mockResolvedValue([createMessage('message-a')]);
const user = userEvent.setup();
renderPage();
+ await selectTopic(user);
await user.click(screen.getByRole('button', { name: /^search查询$/ }));
const row = await screen.findByRole('row', { name: /message-a/ });
@@ -217,6 +236,7 @@ describe('MessagePage async request ownership', () => {
.mockReturnValueOnce(secondQuery.promise);
const user = userEvent.setup();
renderPage();
+ await selectTopic(user);
const queryButton = screen.getByRole('button', { name: /^search查询$/ });
await user.click(queryButton);
@@ -251,6 +271,7 @@ describe('MessagePage async request ownership', () => {
const errorSpy = vi.spyOn(message, 'error').mockImplementation(vi.fn());
const user = userEvent.setup();
renderPage();
+ await selectTopic(user);
await user.click(screen.getByRole('button', { name: /^search查询$/ }));
const row = await screen.findByRole('row', { name: /message-a/ });
@@ -288,6 +309,7 @@ describe('MessagePage async request ownership', () => {
);
const user = userEvent.setup();
renderPage();
+ await selectTopic(user);
await user.click(screen.getByRole('button', { name: /^search查询$/ }));
const firstRow = await screen.findByRole('row', { name: /message-a/ });
@@ -327,6 +349,7 @@ describe('MessagePage async request ownership', () => {
);
const user = userEvent.setup();
renderPage();
+ await selectTopic(user);
await user.click(screen.getByRole('button', { name: /^search查询$/ }));
const firstRow = await screen.findByRole('row', { name: /message-a/ });
diff --git a/web/src/pages/instance/message.tsx
b/web/src/pages/instance/message.tsx
index ee3ba596..5de86438 100644
--- a/web/src/pages/instance/message.tsx
+++ b/web/src/pages/instance/message.tsx
@@ -153,13 +153,47 @@ const isMessageQuery = (value: unknown): value is
MessageQuery => {
);
};
+const getQueryValidationError = (mode: QueryMode, params: MessageQuery):
string | null => {
+ if (!params.topic?.trim()) return '请选择 Topic';
+ if (mode === 'key' && !params.key?.trim()) return '请输入 Message Key';
+ if (mode === 'msgid' && !params.msgId?.trim()) return '请输入 Message ID';
+ return null;
+};
+
+const normalizedText = (value: string | undefined): string | undefined =>
+ value?.trim() || undefined;
+
+const normalizeMessageQuery = (mode: QueryMode, params: MessageQuery):
MessageQuery => {
+ const topic = normalizedText(params.topic);
+ if (mode === 'msgid') {
+ const msgId = normalizedText(params.msgId);
+ return {
+ ...(topic ? { topic } : {}),
+ ...(msgId ? { msgId } : {}),
+ };
+ }
+
+ const tag = normalizedText(params.tag);
+ const commonParams = {
+ ...(topic ? { topic } : {}),
+ ...(tag ? { tag } : {}),
+ ...(params.startTime !== undefined ? { startTime: params.startTime } : {}),
+ ...(params.endTime !== undefined ? { endTime: params.endTime } : {}),
+ };
+ if (mode === 'key') {
+ const key = normalizedText(params.key);
+ return { ...commonParams, ...(key ? { key } : {}) };
+ }
+ return commonParams;
+};
+
const isRecentQuery = (value: unknown): value is RecentQuery => {
if (typeof value !== 'object' || value === null) return false;
const query = value as RecentQuery;
return (
isQueryMode(query.mode) &&
isMessageQuery(query.params) &&
- (query.mode !== 'msgid' || Boolean(query.params.topic?.trim()))
+ getQueryValidationError(query.mode, normalizeMessageQuery(query.mode,
query.params)) === null
);
};
@@ -169,7 +203,10 @@ const loadRecentQueries = (): RecentQuery[] => {
if (!stored) return [];
const parsed: unknown = JSON.parse(stored);
if (!Array.isArray(parsed)) return [];
- return parsed.filter(isRecentQuery).slice(0, MAX_QUERY_HISTORY);
+ return parsed
+ .filter(isRecentQuery)
+ .map(({ mode, params }) => ({ mode, params: normalizeMessageQuery(mode,
params) }))
+ .slice(0, MAX_QUERY_HISTORY);
} catch {
return [];
}
@@ -276,6 +313,15 @@ const MessagePageContent = ({
[],
);
+ const currentQueryParams: MessageQuery =
+ queryMode === 'topic'
+ ? { topic: selectedTopic, startTime: dateRange[0].valueOf(), endTime:
dateRange[1].valueOf() }
+ : queryMode === 'key'
+ ? { topic: selectedTopic, key: keyInput || undefined }
+ : { topic: selectedTopic, msgId: msgIdInput || undefined };
+ const queryValidationError = getQueryValidationError(queryMode,
currentQueryParams);
+ const queryDisabledReason = !selectedInstanceId ? '请先选择实例' :
queryValidationError;
+
/* ─── Handlers ─── */
const handleReset = () => {
queryGenerationRef.current += 1;
@@ -306,20 +352,28 @@ const MessagePageContent = ({
};
const executeQuery = async (mode: QueryMode, params: MessageQuery) => {
+ const requestGeneration = queryGenerationRef.current + 1;
+ queryGenerationRef.current = requestGeneration;
if (!selectedInstanceId) {
setQueryError('请先选择实例后再查询消息');
+ setQueryLoading(false);
+ return;
+ }
+ const normalizedParams = normalizeMessageQuery(mode, params);
+ const validationError = getQueryValidationError(mode, normalizedParams);
+ if (validationError) {
+ setQueryError(validationError);
+ setQueryLoading(false);
return;
}
- const requestGeneration = queryGenerationRef.current + 1;
- queryGenerationRef.current = requestGeneration;
setQueryLoading(true);
setQueryError(null);
try {
- const result = await queryMessages({ ...params, instanceId:
selectedInstanceId });
+ const result = await queryMessages({ ...normalizedParams, instanceId:
selectedInstanceId });
if (queryGenerationRef.current !== requestGeneration) return;
setMessages(result);
setQueryError(null);
- saveRecentQuery(mode, params);
+ saveRecentQuery(mode, normalizedParams);
message.success(`查询完成,共 ${result.length} 条`);
} catch (error) {
if (queryGenerationRef.current === requestGeneration) {
@@ -333,18 +387,7 @@ const MessagePageContent = ({
};
const handleQuery = async () => {
- const params: MessageQuery =
- queryMode === 'topic'
- ? {
- topic: selectedTopic,
- startTime: dateRange[0].valueOf(),
- endTime: dateRange[1].valueOf(),
- }
- : queryMode === 'key'
- ? { topic: selectedTopic, key: keyInput || undefined }
- : { topic: selectedTopic, msgId: msgIdInput || undefined };
-
- await executeQuery(queryMode, params);
+ await executeQuery(queryMode, currentQueryParams);
};
const replayRecentQuery = (recentQuery: RecentQuery) => {
@@ -800,8 +843,8 @@ const MessagePageContent = ({
<Button
type="primary"
icon={<SearchOutlined />}
- disabled={!selectedInstanceId}
- title={selectedInstanceId ? undefined : '请先选择实例'}
+ disabled={Boolean(queryDisabledReason)}
+ title={queryDisabledReason || undefined}
onClick={() => {
void handleQuery();
}}