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 5edf208a fix: handle Cluster page instance bootstrap failures (#1481)
5edf208a is described below

commit 5edf208a4f4e7b1a8addd6493e1f8483a0b09554
Author: aias00 <[email protected]>
AuthorDate: Tue Aug 11 15:38:42 2026 +0800

    fix: handle Cluster page instance bootstrap failures (#1481)
---
 .../pages/cluster/__tests__/ClusterPage.test.tsx   | 50 ++++++++++++++++++++++
 web/src/pages/cluster/index.tsx                    | 43 +++++++++++++++++--
 2 files changed, 90 insertions(+), 3 deletions(-)

diff --git a/web/src/pages/cluster/__tests__/ClusterPage.test.tsx 
b/web/src/pages/cluster/__tests__/ClusterPage.test.tsx
index ca117fd8..22af9793 100644
--- a/web/src/pages/cluster/__tests__/ClusterPage.test.tsx
+++ b/web/src/pages/cluster/__tests__/ClusterPage.test.tsx
@@ -32,7 +32,12 @@ const clusterServiceMocks = vi.hoisted(() => ({
   updateNameServer: vi.fn(),
 }));
 
+const instanceServiceMocks = vi.hoisted(() => ({
+  listInstances: vi.fn(),
+}));
+
 vi.mock('../../../services/clusterService', () => clusterServiceMocks);
+vi.mock('../../../services/instanceService', () => instanceServiceMocks);
 
 import ClusterPage from '../index';
 
@@ -132,6 +137,20 @@ const flushPromises = async () => {
 
 describe('Cluster page', () => {
   beforeEach(() => {
+    instanceServiceMocks.listInstances.mockReset().mockResolvedValue([
+      {
+        id: 'instance-1',
+        name: 'Instance 1',
+        endpoint: 'namesrv-1:9876',
+        type: 'DIRECT',
+        vendor: 'APACHE',
+        remark: '',
+        topicCount: 0,
+        consumerGroupCount: 0,
+        createdAt: '',
+        updatedAt: '',
+      },
+    ]);
     
clusterServiceMocks.createNameServer.mockReset().mockResolvedValue(undefined);
     
clusterServiceMocks.listClusters.mockReset().mockResolvedValue([buildCluster()]);
     clusterServiceMocks.restartProxy.mockReset().mockResolvedValue(undefined);
@@ -154,6 +173,34 @@ describe('Cluster page', () => {
     vi.restoreAllMocks();
   });
 
+  it('surfaces instance bootstrap failures and retries without querying a 
default cluster', async () => {
+    instanceServiceMocks.listInstances
+      .mockRejectedValueOnce(new Error('managed instances unavailable'))
+      .mockResolvedValueOnce([
+        {
+          id: 'instance-1',
+          name: 'Instance 1',
+          endpoint: 'namesrv-1:9876',
+          type: 'DIRECT',
+          vendor: 'APACHE',
+          remark: '',
+          topicCount: 0,
+          consumerGroupCount: 0,
+          createdAt: '',
+          updatedAt: '',
+        },
+      ]);
+    const user = userEvent.setup();
+    renderWithProviders(<ClusterPage />);
+
+    const alert = await screen.findByRole('alert');
+    expect(clusterServiceMocks.listClusters).not.toHaveBeenCalled();
+
+    await user.click(within(alert).getByRole('button', { name: /重\s*试/ }));
+    expect(await screen.findByText('rocketmq-prod-0')).toBeInTheDocument();
+    
expect(clusterServiceMocks.listClusters).toHaveBeenCalledWith('instance-1');
+  });
+
   it('opens proxy detail dialog from the proxy table', async () => {
     const user = userEvent.setup();
     renderWithProviders(<ClusterPage />);
@@ -331,6 +378,7 @@ describe('Cluster page', () => {
       .mockResolvedValueOnce([buildCluster({ tpsIn: 202 })]);
 
     renderWithProviders(<ClusterPage />);
+    await flushPromises();
     fireEvent.click(screen.getByRole('button', { name: '刷新' }));
     fireEvent.click(screen.getByRole('button', { name: '刷新' }));
 
@@ -475,6 +523,8 @@ describe('Cluster page', () => {
     clusterServiceMocks.listClusters.mockReturnValue(initialRequest.promise);
     const view = renderWithProviders(<ClusterPage />);
 
+    await flushPromises();
+
     view.unmount();
     await act(async () => {
       initialRequest.resolve([buildCluster()]);
diff --git a/web/src/pages/cluster/index.tsx b/web/src/pages/cluster/index.tsx
index 41057455..1c8dacdb 100644
--- a/web/src/pages/cluster/index.tsx
+++ b/web/src/pages/cluster/index.tsx
@@ -34,6 +34,7 @@ import {
   Space,
   Typography,
   Card,
+  Alert,
   message,
 } from 'antd';
 import type { ColumnsType } from 'antd/es/table';
@@ -87,6 +88,8 @@ const ClusterPage = () => {
   const [clusters, setClusters] = useState<ClusterInfo[]>([]);
   const [instances, setInstances] = useState<Instance[]>([]);
   const [selectedInstanceId, setSelectedInstanceId] = useState('');
+  const [instanceLoadError, setInstanceLoadError] = useState<string | 
null>(null);
+  const [instanceLoadKey, setInstanceLoadKey] = useState(0);
   const [loading, setLoading] = useState(true);
   const [nsSearch, setNsSearch] = useState('');
   const [brokerSearch, setBrokerSearch] = useState('');
@@ -155,19 +158,34 @@ const ClusterPage = () => {
   const selectedInstanceIdRef = useRef('');
 
   useEffect(() => {
+    let cancelled = false;
     void listInstances()
       .then((nextInstances) => {
+        if (cancelled) return;
         const apacheInstances = nextInstances.filter((instance) => 
instance.vendor === 'APACHE');
         setInstances(apacheInstances);
         const initialInstanceId = apacheInstances[0]?.id ?? '';
         selectedInstanceIdRef.current = initialInstanceId;
         setSelectedInstanceId(initialInstanceId);
-        void requestRefreshRef.current('manual');
+        setInstanceLoadError(null);
+        if (initialInstanceId) void requestRefreshRef.current('manual');
       })
       .catch(() => {
-        // Keep the page usable without instance filtering when the instance 
list is unavailable.
+        if (cancelled) return;
+        selectedInstanceIdRef.current = '';
+        setInstances([]);
+        setSelectedInstanceId('');
+        setClusters([]);
+        setSelectedProxy(null);
+        setInstanceLoadError(tRef.current('common.fetchDataFailed'));
+        setLoading(false);
+        setAutoRefresh(false);
+        autoRefreshRef.current = false;
       });
-  }, []);
+    return () => {
+      cancelled = true;
+    };
+  }, [instanceLoadKey]);
 
   const clearRefreshTimer = useCallback(() => {
     if (refreshTimerRef.current !== null) {
@@ -181,6 +199,12 @@ const ClusterPage = () => {
       clearRefreshTimer();
       if (!mountedRef.current) return Promise.resolve();
 
+      if (!selectedInstanceIdRef.current) {
+        setClusters([]);
+        setLoading(false);
+        return Promise.resolve();
+      }
+
       if (source !== 'background') setLoading(true);
 
       if (inFlightRefreshRef.current) {
@@ -1029,6 +1053,19 @@ const ClusterPage = () => {
           </Flex>
         }
       />
+      {instanceLoadError && (
+        <Alert
+          type="error"
+          showIcon
+          message={instanceLoadError}
+          action={
+            <Button size="small" onClick={() => setInstanceLoadKey((key) => 
key + 1)}>
+              {t('common.retry')}
+            </Button>
+          }
+          style={{ marginBottom: 16 }}
+        />
+      )}
       <style>{`
         @keyframes livePulse {
           0%, 100% { opacity: 1; box-shadow: 0 0 0 0 rgba(82, 196, 26, 0.4); }

Reply via email to