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 283350f1 fix(cluster): scope legacy Broker diagnostics and hide 
unavailable actions (#1541)
283350f1 is described below

commit 283350f1d78ebaa5aa7404c8cc2a4ab5192e5af1
Author: aias00 <[email protected]>
AuthorDate: Tue Aug 11 20:22:26 2026 +0800

    fix(cluster): scope legacy Broker diagnostics and hide unavailable actions 
(#1541)
    
    * fix(cluster): scope legacy Broker diagnostics by instance
    
    Signed-off-by: liuhy <[email protected]>
    
    * test(cluster): cover legacy Broker instance discovery failure
    
    Signed-off-by: liuhy <[email protected]>
    
    * fix(cluster): hide unsupported legacy Broker restart
    
    Signed-off-by: liuhy <[email protected]>
    
    ---------
    
    Signed-off-by: liuhy <[email protected]>
---
 web/src/pages/studio/BrokerCluster.tsx             | 93 ++++++++++------------
 .../pages/studio/__tests__/BrokerCluster.test.tsx  | 54 +++++++------
 2 files changed, 75 insertions(+), 72 deletions(-)

diff --git a/web/src/pages/studio/BrokerCluster.tsx 
b/web/src/pages/studio/BrokerCluster.tsx
index e9f1b549..6bddb01f 100644
--- a/web/src/pages/studio/BrokerCluster.tsx
+++ b/web/src/pages/studio/BrokerCluster.tsx
@@ -25,21 +25,21 @@ import {
   Space,
   Switch,
   Progress,
-  Tooltip,
   Spin,
   App,
-  Modal,
+  Select,
 } from 'antd';
 import {
   ArrowClockwise,
-  ArrowsClockwise,
   Cloud,
   ChartBar,
   PlugsConnected,
 } from '@phosphor-icons/react';
 import { useLang } from '../../i18n/LangContext';
-import { listClusters, restartBroker } from '../../services/clusterService';
+import { listClusters } from '../../services/clusterService';
 import type { ClusterInfo } from '../../api/cluster';
+import { listInstances } from '../../services/instanceService';
+import type { Instance } from '../../api/instance';
 
 // ─── Types ──────────────────────────────────────────────────────
 type NodeStatus = 'running' | 'readonly' | 'maintenance' | 'unknown';
@@ -165,15 +165,27 @@ const BrokerClusterPage = () => {
   const [brokerData, setBrokerData] = useState<BrokerRecord[]>([]);
   const [nameServerData, setNameServerData] = useState<NameServerRecord[]>([]);
   const [proxyData, setProxyData] = useState<ProxyRecord[]>([]);
+  const [instances, setInstances] = useState<Instance[]>([]);
+  const [selectedInstanceId, setSelectedInstanceId] = useState('');
   const loadRequestId = useRef(0);
   const { t } = useLang();
   const { message } = App.useApp();
 
+  const clearData = useCallback(() => {
+    setBrokerData([]);
+    setNameServerData([]);
+    setProxyData([]);
+  }, []);
+
   const loadData = useCallback(async () => {
+    if (!selectedInstanceId) {
+      clearData();
+      return;
+    }
     const requestId = ++loadRequestId.current;
     setLoading(true);
     try {
-      const clusters = await listClusters();
+      const clusters = await listClusters(selectedInstanceId);
       if (requestId !== loadRequestId.current) return;
       const mapped = mapClusters(clusters);
       setBrokerData(mapped.brokers);
@@ -187,30 +199,29 @@ const BrokerClusterPage = () => {
         setLoading(false);
       }
     }
-  }, [message, t]);
+  }, [clearData, message, selectedInstanceId, t]);
 
-  const handleRestartBroker = async (broker: BrokerRecord) => {
-    try {
-      const result = await restartBroker(broker.clusterId, broker.brokerName);
-      if (!result.success) {
-        message.error(result.message || t('common.failure'));
-        return;
-      }
-      await loadData();
-      message.success(
-        result.message || t('cluster.restartBrokerSubmitted', { name: 
broker.brokerName }),
-      );
-    } catch {
-      message.error(t('common.failure'));
-    }
-  };
+  useEffect(() => {
+    let active = true;
+    void listInstances()
+      .then((nextInstances) => {
+        if (!active) return;
+        setInstances(nextInstances);
+        setSelectedInstanceId(nextInstances[0]?.id ?? '');
+      })
+      .catch(() => {
+        if (!active) return;
+        clearData();
+        message.error(t('common.fetchDataFailed'));
+      });
+    return () => {
+      active = false;
+    };
+  }, [clearData, message, t]);
 
   useEffect(() => {
-    const timeoutId = window.setTimeout(() => {
-      void loadData();
-    });
+    void loadData();
     return () => {
-      window.clearTimeout(timeoutId);
       ++loadRequestId.current;
     };
   }, [loadData]);
@@ -325,30 +336,6 @@ const BrokerClusterPage = () => {
       ),
       sorter: (a: BrokerRecord, b: BrokerRecord) => (a.tpsOut ?? -1) - 
(b.tpsOut ?? -1),
     },
-    {
-      title: t('common.actions'),
-      key: 'action',
-      render: (_: unknown, record: BrokerRecord) => (
-        <Tooltip title={t('brokerCluster.restart')}>
-          <Button
-            type="link"
-            size="small"
-            icon={<ArrowsClockwise size={14} />}
-            onClick={() => {
-              Modal.confirm({
-                title: t('cluster.confirmRestart'),
-                content: t('cluster.restartBrokerConfirm', { name: 
record.brokerName }),
-                okText: t('common.confirm'),
-                cancelText: t('common.cancel'),
-                onOk: () => handleRestartBroker(record),
-              });
-            }}
-          >
-            {t('brokerCluster.restart')}
-          </Button>
-        </Tooltip>
-      ),
-    },
   ];
 
   const nsColumns = [
@@ -481,6 +468,14 @@ const BrokerClusterPage = () => {
           {t('brokerCluster.title')}
         </h2>
         <Space size="middle">
+          <Select
+            aria-label="选择实例"
+            value={selectedInstanceId || undefined}
+            onChange={setSelectedInstanceId}
+            placeholder="选择实例"
+            style={{ minWidth: 180 }}
+            options={instances.map((instance) => ({ value: instance.id, label: 
instance.name }))}
+          />
           <Switch
             checked={autoRefresh}
             onChange={setAutoRefresh}
diff --git a/web/src/pages/studio/__tests__/BrokerCluster.test.tsx 
b/web/src/pages/studio/__tests__/BrokerCluster.test.tsx
index 5bb1aa48..1c54da3a 100644
--- a/web/src/pages/studio/__tests__/BrokerCluster.test.tsx
+++ b/web/src/pages/studio/__tests__/BrokerCluster.test.tsx
@@ -20,13 +20,17 @@ import { act, fireEvent, render, screen, waitFor } from 
'@testing-library/react'
 import userEvent from '@testing-library/user-event';
 import { App } from 'antd';
 import { LangProvider } from '../../../i18n/LangContext';
-import { listClusters, restartBroker } from '../../../services/clusterService';
+import { listClusters } from '../../../services/clusterService';
+import { listInstances } from '../../../services/instanceService';
 import type { ClusterInfo } from '../../../api/cluster';
 import BrokerCluster from '../BrokerCluster';
 
 vi.mock('../../../services/clusterService', () => ({
   listClusters: vi.fn(),
-  restartBroker: vi.fn(),
+}));
+
+vi.mock('../../../services/instanceService', () => ({
+  listInstances: vi.fn(),
 }));
 
 // Mock matchMedia for antd responsive components
@@ -128,8 +132,20 @@ const createDeferred = <T,>() => {
 describe('BrokerCluster Page', () => {
   beforeEach(() => {
     vi.clearAllMocks();
+    vi.mocked(listInstances).mockResolvedValue([
+      {
+        id: 'instance-1',
+        name: 'prod-cn',
+        remark: '',
+        type: 'DIRECT',
+        endpoint: '10.0.1.20:9876',
+        topicCount: 0,
+        consumerGroupCount: 0,
+        createdAt: '',
+        updatedAt: '',
+      },
+    ]);
     vi.mocked(listClusters).mockResolvedValue(clusterFixture);
-    vi.mocked(restartBroker).mockResolvedValue({ success: true, message: 
'restarted' });
   });
 
   afterEach(() => {
@@ -153,6 +169,16 @@ describe('BrokerCluster Page', () => {
     expect(brokerA.length).toBeGreaterThan(0);
     expect(screen.getAllByText('broker-api-b').length).toBeGreaterThan(0);
     expect(screen.queryByText('broker-a')).not.toBeInTheDocument();
+    expect(listClusters).toHaveBeenCalledWith('instance-1');
+  });
+
+  it('does not fall back to an unscoped cluster query when instance discovery 
fails', async () => {
+    vi.mocked(listInstances).mockRejectedValueOnce(new Error('instance 
discovery failed'));
+    renderWithProviders(<BrokerCluster />);
+
+    await waitFor(() => expect(listInstances).toHaveBeenCalledTimes(1));
+    expect(listClusters).not.toHaveBeenCalled();
+    expect(screen.queryByText('broker-api-a')).not.toBeInTheDocument();
   });
 
   it('should display broker status tags', async () => {
@@ -185,30 +211,12 @@ describe('BrokerCluster Page', () => {
     expect(screen.getAllByText('10.0.1.30:8080').length).toBeGreaterThan(0);
   });
 
-  it('should render only supported broker restart actions', async () => {
+  it('renders Broker runtime data without unavailable mutation actions', async 
() => {
     renderWithProviders(<BrokerCluster />);
     await screen.findByText('broker-api-a');
     expect(screen.queryByText('创建集群')).not.toBeInTheDocument();
     expect(screen.queryByText('配置')).not.toBeInTheDocument();
-    const restartButtons = screen.getAllByText('重启');
-    expect(restartButtons).toHaveLength(2);
-  });
-
-  it('restarts a broker after confirmation and refreshes the cluster data', 
async () => {
-    const user = userEvent.setup();
-    renderWithProviders(<BrokerCluster />);
-    await screen.findByText('broker-api-a');
-
-    await user.click(screen.getAllByText('重启')[0]);
-    expect(await screen.findByText('确定要重启 Broker "broker-api-a" 
吗?')).toBeInTheDocument();
-    await user.click(screen.getByRole('button', { name: /确\s*认/ }));
-
-    await waitFor(() => {
-      expect(restartBroker).toHaveBeenCalledWith('cluster-1', 'broker-api-a');
-    });
-    await waitFor(() => {
-      expect(listClusters).toHaveBeenCalledTimes(2);
-    });
+    expect(screen.queryByText('重启')).not.toBeInTheDocument();
   });
 
   it('does not show mock infrastructure data when the API fails', async () => {

Reply via email to