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 () => {