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 7f0b0607 fix: surface unavailable runtime providers (#946)
7f0b0607 is described below

commit 7f0b0607c496ca3a7b6956d21c293c321a4fe3cb
Author: aias00 <[email protected]>
AuthorDate: Tue Aug 4 03:06:38 2026 -0700

    fix: surface unavailable runtime providers (#946)
    
    * fix: surface unavailable message provider errors
    
    * fix: surface unavailable dlq provider errors
    
    * fix: surface unavailable client provider errors
---
 .../pages/cluster/__tests__/ClientsPage.test.tsx   | 12 ++++++
 web/src/pages/cluster/clients.tsx                  | 40 ++++++++++++++++--
 web/src/pages/instance/__tests__/DLQPage.test.tsx  | 26 ++++++++++++
 .../__tests__/MessagePageAsyncState.test.tsx       | 30 ++++++++++++++
 web/src/pages/instance/dlq.tsx                     | 47 ++++++++++++++++++++--
 web/src/pages/instance/message.tsx                 | 46 +++++++++++++++++++--
 6 files changed, 189 insertions(+), 12 deletions(-)

diff --git a/web/src/pages/cluster/__tests__/ClientsPage.test.tsx 
b/web/src/pages/cluster/__tests__/ClientsPage.test.tsx
index beb6b5e6..d72367a5 100644
--- a/web/src/pages/cluster/__tests__/ClientsPage.test.tsx
+++ b/web/src/pages/cluster/__tests__/ClientsPage.test.tsx
@@ -187,6 +187,18 @@ describe('Clients page', () => {
     ).toBeInTheDocument();
   });
 
+  it('surfaces unavailable provider errors from the client API', async () => {
+    vi.mocked(connectionsService.listConnections).mockRejectedValue(
+      new Error('Client connection provider is not configured'),
+    );
+    renderWithProviders(<ClientsPage />);
+
+    expect(
+      await screen.findByText('Client connection provider is not configured'),
+    ).toBeInTheDocument();
+    
expect(within(screen.getByTestId('connection-total')).getByText('0')).toBeInTheDocument();
+  });
+
   it('opens a client detail dialog from the connection table', async () => {
     const user = userEvent.setup();
     renderWithProviders(<ClientsPage />);
diff --git a/web/src/pages/cluster/clients.tsx 
b/web/src/pages/cluster/clients.tsx
index baa8d789..5c7581e3 100644
--- a/web/src/pages/cluster/clients.tsx
+++ b/web/src/pages/cluster/clients.tsx
@@ -17,6 +17,7 @@
 
 import { useEffect, useMemo, useState } from 'react';
 import {
+  Alert,
   Button,
   Card,
   Descriptions,
@@ -29,7 +30,6 @@ import {
   Table,
   Tag,
   Typography,
-  message,
   theme,
 } from 'antd';
 import { Eye, MagnifyingGlass } from '@phosphor-icons/react';
@@ -42,6 +42,7 @@ import { listConnections } from 
'../../services/connectionsService';
 import { formatDateTime } from '../../utils/format';
 
 const { Text } = Typography;
+const DEFAULT_LOAD_ERROR = '客户端连接加载失败,请稍后重试';
 
 /* ─── Helpers ─── */
 
@@ -76,6 +77,27 @@ const countBy = (values: string[]) =>
     .map(([label, count]) => ({ label, count }))
     .sort((a, b) => b.count - a.count || a.label.localeCompare(b.label));
 
+type ApiErrorLike = {
+  message?: unknown;
+  response?: {
+    data?: {
+      message?: unknown;
+    };
+  };
+};
+
+function getLoadErrorMessage(error: unknown): string {
+  const apiError = error as ApiErrorLike;
+  const responseMessage = apiError.response?.data?.message;
+  if (typeof responseMessage === 'string' && responseMessage.trim()) {
+    return responseMessage;
+  }
+  if (typeof apiError.message === 'string' && apiError.message.trim()) {
+    return apiError.message;
+  }
+  return DEFAULT_LOAD_ERROR;
+}
+
 /* ═══════════════════════════════════════════
    ClientsPage
    ═══════════════════════════════════════════ */
@@ -87,16 +109,22 @@ const ClientsPage = () => {
   const [search, setSearch] = useState('');
   const [clusterFilter, setClusterFilter] = useState<string>('ALL');
   const [selectedConnection, setSelectedConnection] = 
useState<ClientConnection | null>(null);
+  const [loadError, setLoadError] = useState<string | null>(null);
 
   useEffect(() => {
     let cancelled = false;
 
     void listConnections()
       .then((nextConnections) => {
-        if (!cancelled) setConnections(nextConnections);
+        if (!cancelled) {
+          setConnections(nextConnections);
+          setLoadError(null);
+        }
       })
-      .catch(() => {
-        if (!cancelled) message.error('客户端连接加载失败,请稍后重试');
+      .catch((error) => {
+        if (!cancelled) {
+          setLoadError(getLoadErrorMessage(error));
+        }
       })
       .finally(() => {
         if (!cancelled) setLoading(false);
@@ -298,6 +326,10 @@ const ClientsPage = () => {
         subtitle={`${t('clients.title')} — ${filtered.length} connections`}
       />
 
+      {loadError && (
+        <Alert showIcon type="warning" message={loadError} style={{ 
marginBottom: 16 }} />
+      )}
+
       {/* ─── Filter Bar ─── */}
       <Flex justify="space-between" align="center" style={{ marginBottom: 16 
}}>
         <Space size={12} wrap>
diff --git a/web/src/pages/instance/__tests__/DLQPage.test.tsx 
b/web/src/pages/instance/__tests__/DLQPage.test.tsx
index bd5d2b82..4c1b8adb 100644
--- a/web/src/pages/instance/__tests__/DLQPage.test.tsx
+++ b/web/src/pages/instance/__tests__/DLQPage.test.tsx
@@ -110,6 +110,15 @@ describe('DLQ page', () => {
     expect(messageService.listDLQGroups).toHaveBeenCalledTimes(1);
   });
 
+  it('surfaces unavailable DLQ provider errors when loading groups', async () 
=> {
+    vi.mocked(messageService.listDLQGroups).mockRejectedValue(
+      new Error('DLQ provider is not configured'),
+    );
+    renderWithProviders(<DLQPage />);
+
+    expect(await screen.findByText('DLQ provider is not 
configured')).toBeInTheDocument();
+  });
+
   it('opens a detail dialog with the selected group metadata', async () => {
     const user = userEvent.setup();
     renderWithProviders(<DLQPage />);
@@ -181,4 +190,21 @@ describe('DLQ page', () => {
     await waitFor(() => 
expect(within(orderRow).getByRole('checkbox')).toBeDisabled());
     expect(screen.getByRole('button', { name: /批量导出/ })).toBeDisabled();
   });
+
+  it('surfaces unavailable DLQ provider errors when retry submission fails', 
async () => {
+    vi.mocked(messageService.resendDLQ).mockRejectedValue(
+      new Error('DLQ provider is not configured'),
+    );
+    const user = userEvent.setup();
+    renderWithProviders(<DLQPage />);
+
+    const orderRow = (await screen.findByText('cg-order')).closest('tr');
+    if (!orderRow) throw new Error('DLQ group row not found');
+
+    await user.click(within(orderRow).getByRole('button', { name: '重投消息' }));
+    await user.type(screen.getByPlaceholderText('输入目标 Topic 名称'), 
'orders-retry');
+    await user.click(screen.getByRole('button', { name: '确认重投' }));
+
+    expect(await screen.findByText('DLQ provider is not 
configured')).toBeInTheDocument();
+  });
 });
diff --git a/web/src/pages/instance/__tests__/MessagePageAsyncState.test.tsx 
b/web/src/pages/instance/__tests__/MessagePageAsyncState.test.tsx
index a2ebe31a..fc7c0fc3 100644
--- a/web/src/pages/instance/__tests__/MessagePageAsyncState.test.tsx
+++ b/web/src/pages/instance/__tests__/MessagePageAsyncState.test.tsx
@@ -130,6 +130,36 @@ describe('MessagePage async request ownership', () => {
 
     expect(screen.queryByText('late-after-reset')).not.toBeInTheDocument();
   });
+  it('surfaces unavailable message provider errors from query requests', async 
() => {
+    serviceMocks.queryMessages.mockRejectedValue(
+      new Error('Message query provider is not configured'),
+    );
+    const user = userEvent.setup();
+    renderPage();
+
+    await user.click(screen.getByRole('button', { name: /^search查询$/ }));
+
+    expect(await screen.findByText('Message query provider is not 
configured')).toBeInTheDocument();
+    expect(screen.getByRole('button', { name: /最近查询/ })).toBeDisabled();
+  });
+
+  it('surfaces unavailable message provider errors from trace requests', async 
() => {
+    serviceMocks.queryMessages.mockResolvedValue([createMessage('message-a')]);
+    serviceMocks.getMessageTrace.mockRejectedValue(
+      new Error('Message query provider is not configured'),
+    );
+    const user = userEvent.setup();
+    renderPage();
+
+    await user.click(screen.getByRole('button', { name: /^search查询$/ }));
+    const row = await screen.findByRole('row', { name: /message-a/ });
+    await user.click(within(row).getByRole('button', { name: /轨迹/ }));
+
+    const dialog = await screen.findByRole('dialog', { name: '消息详情' });
+    expect(
+      await within(dialog).findByText('Message query provider is not 
configured'),
+    ).toBeInTheDocument();
+  });
 
   it('keeps normal message resend disabled until a real API is wired', async 
() => {
     serviceMocks.queryMessages.mockResolvedValue([createMessage('message-a')]);
diff --git a/web/src/pages/instance/dlq.tsx b/web/src/pages/instance/dlq.tsx
index 3ff3e191..8f3cdf33 100644
--- a/web/src/pages/instance/dlq.tsx
+++ b/web/src/pages/instance/dlq.tsx
@@ -17,6 +17,7 @@
 
 import { useEffect, useMemo, useState } from 'react';
 import {
+  Alert,
   Card,
   Table,
   Button,
@@ -42,9 +43,32 @@ import { downloadBlob } from '../../utils/download';
 
 const { Text } = Typography;
 const { RangePicker } = DatePicker;
+const DEFAULT_LOAD_ERROR = '死信队列加载失败,请稍后重试';
+const DEFAULT_RETRY_ERROR = '提交重投任务失败,请稍后重试';
 
 /* ─── Helpers ─── */
 
+type ApiErrorLike = {
+  message?: unknown;
+  response?: {
+    data?: {
+      message?: unknown;
+    };
+  };
+};
+
+const getErrorMessage = (error: unknown, fallback: string): string => {
+  const apiError = error as ApiErrorLike;
+  const responseMessage = apiError.response?.data?.message;
+  if (typeof responseMessage === 'string' && responseMessage.trim()) {
+    return responseMessage;
+  }
+  if (typeof apiError.message === 'string' && apiError.message.trim()) {
+    return apiError.message;
+  }
+  return fallback;
+};
+
 const formatDateTime = (iso: string): string => {
   const d = new Date(iso);
   const pad = (n: number) => String(n).padStart(2, '0');
@@ -93,6 +117,8 @@ const DLQPage = () => {
   const [retrySubmitting, setRetrySubmitting] = useState(false);
   const [detailGroup, setDetailGroup] = useState<DLQGroup | null>(null);
   const [selectedGroupNames, setSelectedGroupNames] = useState<string[]>([]);
+  const [loadError, setLoadError] = useState<string | null>(null);
+  const [retryError, setRetryError] = useState<string | null>(null);
 
   useEffect(() => {
     let cancelled = false;
@@ -101,6 +127,7 @@ const DLQPage = () => {
       .then((nextGroups) => {
         if (!cancelled) {
           setGroups(nextGroups);
+          setLoadError(null);
           const availableGroups = new Set(
             nextGroups.filter((group) => group.messageCount > 0).map((group) 
=> group.groupName),
           );
@@ -109,8 +136,8 @@ const DLQPage = () => {
           );
         }
       })
-      .catch(() => {
-        if (!cancelled) message.error('死信队列加载失败,请稍后重试');
+      .catch((error) => {
+        if (!cancelled) setLoadError(getErrorMessage(error, 
DEFAULT_LOAD_ERROR));
       })
       .finally(() => {
         if (!cancelled) setLoading(false);
@@ -140,6 +167,7 @@ const DLQPage = () => {
     setRetryGroup(group);
     setRetryRange([dayjs().subtract(1, 'day'), dayjs()]);
     setRetryTargetTopic('');
+    setRetryError(null);
     setRetryModalOpen(true);
   };
 
@@ -151,6 +179,7 @@ const DLQPage = () => {
     if (!retryGroup) return;
 
     setRetrySubmitting(true);
+    setRetryError(null);
     try {
       await resendDLQ({
         groupName: retryGroup.groupName,
@@ -162,8 +191,9 @@ const DLQPage = () => {
       message.success(`已提交重投任务:${retryGroup.groupName} → ${retryTargetTopic}`);
       setRetryModalOpen(false);
       setRetryGroup(null);
-    } catch {
-      message.error('提交重投任务失败,请稍后重试');
+      setRetryError(null);
+    } catch (error) {
+      setRetryError(getErrorMessage(error, DEFAULT_RETRY_ERROR));
     } finally {
       setRetrySubmitting(false);
     }
@@ -308,6 +338,10 @@ const DLQPage = () => {
         </Button>
       </Flex>
 
+      {loadError && (
+        <Alert showIcon type="warning" message={loadError} style={{ 
marginBottom: 16 }} />
+      )}
+
       {/* ── Table ── */}
       <Card bodyStyle={{ padding: 0 }}>
         <Table
@@ -344,6 +378,7 @@ const DLQPage = () => {
         onCancel={() => {
           setRetryModalOpen(false);
           setRetryGroup(null);
+          setRetryError(null);
         }}
         onOk={handleRetry}
         confirmLoading={retrySubmitting}
@@ -354,6 +389,10 @@ const DLQPage = () => {
       >
         {retryGroup && (
           <div style={{ marginTop: 16 }}>
+            {retryError && (
+              <Alert showIcon type="warning" message={retryError} style={{ 
marginBottom: 16 }} />
+            )}
+
             <div
               style={{
                 marginBottom: 16,
diff --git a/web/src/pages/instance/message.tsx 
b/web/src/pages/instance/message.tsx
index ee56b2b6..5162392f 100644
--- a/web/src/pages/instance/message.tsx
+++ b/web/src/pages/instance/message.tsx
@@ -17,6 +17,7 @@
 
 import { useEffect, useRef, useState } from 'react';
 import {
+  Alert,
   Card,
   Table,
   Tag,
@@ -60,6 +61,8 @@ import { downloadBlob } from '../../utils/download';
 
 const { Paragraph, Text } = Typography;
 const { RangePicker } = DatePicker;
+const DEFAULT_QUERY_ERROR = '消息查询失败,请稍后重试';
+const DEFAULT_TRACE_ERROR = '消息轨迹加载失败,请稍后重试';
 
 /* ─── Constants ─── */
 
@@ -70,6 +73,15 @@ type RecentQuery = {
   params: MessageQuery;
 };
 
+type ApiErrorLike = {
+  message?: unknown;
+  response?: {
+    data?: {
+      message?: unknown;
+    };
+  };
+};
+
 const QUERY_HISTORY_STORAGE_KEY = 'rocketmq-studio-message-query-history';
 const MAX_QUERY_HISTORY = 5;
 const RESEND_UNAVAILABLE_MESSAGE = '当前版本尚未接入普通消息重新发送接口';
@@ -179,6 +191,18 @@ const queryLabel = ({ mode, params }: RecentQuery): string 
=> {
   return `Topic: ${params.topic || '全部'}`;
 };
 
+const getErrorMessage = (error: unknown, fallback: string): string => {
+  const apiError = error as ApiErrorLike;
+  const responseMessage = apiError.response?.data?.message;
+  if (typeof responseMessage === 'string' && responseMessage.trim()) {
+    return responseMessage;
+  }
+  if (typeof apiError.message === 'string' && apiError.message.trim()) {
+    return apiError.message;
+  }
+  return fallback;
+};
+
 /* ═══════════════════════════════════════════
    MessagePage
    ═══════════════════════════════════════════ */
@@ -218,6 +242,8 @@ const MessagePage = () => {
   const [selectedMsg, setSelectedMsg] = useState<MessageRecord | null>(null);
   const [traceData, setTraceData] = useState<TraceRecord | null>(null);
   const [traceLoading, setTraceLoading] = useState(false);
+  const [queryError, setQueryError] = useState<string | null>(null);
+  const [traceError, setTraceError] = useState<string | null>(null);
   const [recentQueries, setRecentQueries] = 
useState<RecentQuery[]>(loadRecentQueries);
   const queryGenerationRef = useRef(0);
   const traceGenerationRef = useRef(0);
@@ -238,6 +264,7 @@ const MessagePage = () => {
     setMsgIdInput('');
     setDateRange(getDefaultRange());
     setMessages([]);
+    setQueryError(null);
     setQueryLoading(false);
   };
 
@@ -262,15 +289,17 @@ const MessagePage = () => {
     const requestGeneration = queryGenerationRef.current + 1;
     queryGenerationRef.current = requestGeneration;
     setQueryLoading(true);
+    setQueryError(null);
     try {
       const result = await queryMessages(params);
       if (queryGenerationRef.current !== requestGeneration) return;
       setMessages(result);
+      setQueryError(null);
       saveRecentQuery(mode, params);
       message.success(`查询完成,共 ${result.length} 条`);
-    } catch {
+    } catch (error) {
       if (queryGenerationRef.current === requestGeneration) {
-        message.error('消息查询失败,请稍后重试');
+        setQueryError(getErrorMessage(error, DEFAULT_QUERY_ERROR));
       }
     } finally {
       if (queryGenerationRef.current === requestGeneration) {
@@ -360,13 +389,15 @@ const MessagePage = () => {
     setModalOpen(true);
     setTraceData(null);
     setTraceLoading(true);
+    setTraceError(null);
     try {
       const result = await getMessageTrace(record.msgId);
       if (traceGenerationRef.current !== requestGeneration) return;
       setTraceData(result);
-    } catch {
+      setTraceError(null);
+    } catch (error) {
       if (traceGenerationRef.current === requestGeneration) {
-        message.error('消息轨迹加载失败,请稍后重试');
+        setTraceError(getErrorMessage(error, DEFAULT_TRACE_ERROR));
       }
     } finally {
       if (traceGenerationRef.current === requestGeneration) {
@@ -379,6 +410,7 @@ const MessagePage = () => {
     traceGenerationRef.current += 1;
     setModalOpen(false);
     setTraceLoading(false);
+    setTraceError(null);
   };
 
   const handleDownload = (record: MessageRecord) => {
@@ -600,6 +632,8 @@ const MessagePage = () => {
       label: '消息轨迹',
       children: traceLoading ? (
         <Typography.Text type="secondary">正在加载轨迹数据…</Typography.Text>
+      ) : traceError ? (
+        <Alert showIcon type="warning" message={traceError} />
       ) : traceData?.nodes?.length ? (
         <Steps
           direction="vertical"
@@ -748,6 +782,10 @@ const MessagePage = () => {
         </Space>
       </Card>
 
+      {queryError && (
+        <Alert showIcon type="warning" message={queryError} style={{ 
marginBottom: 16 }} />
+      )}
+
       {/* ── Results Table ── */}
       <Card bodyStyle={{ padding: 0 }}>
         <Table

Reply via email to