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();
               }}

Reply via email to