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