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 eca042d3c fix(cluster): keep the selected instance when the display
language changes (#4590)
eca042d3c is described below
commit eca042d3c623ae3d8ebdb48979b3193bb49a7dd2
Author: 烤化の初雪 <[email protected]>
AuthorDate: Mon Sep 21 21:03:28 2026 +0800
fix(cluster): keep the selected instance when the display language changes
(#4590)
The instance discovery effect in `web/src/pages/studio/BrokerCluster.tsx`
depends on `t`, which is rebuilt when the display language changes, and it
ended with an unconditional `setSelectedInstanceId(apacheInstances[0]?.name)`.
Switching the language in the top bar therefore re-ran `listInstances()` and
reset the page to the first instance, reloading the broker, NameServer and
proxy topology below it with another instance's data — from the operator's
point of view, changing the langua [...]
The setter is now functional: the current selection is kept when it still
appears in the freshly discovered list and falls back to the first instance
otherwise, so first load and instance deletion behave exactly as before.
`Producer.tsx` and `useInstanceFilter` already guard the same way; this was the
remaining unguarded instance selection.
Fixes #4589
---
web/src/pages/studio/BrokerCluster.tsx | 6 +-
.../__tests__/BrokerClusterInstanceScope.test.tsx | 185 +++++++++++++++++++++
2 files changed, 190 insertions(+), 1 deletion(-)
diff --git a/web/src/pages/studio/BrokerCluster.tsx
b/web/src/pages/studio/BrokerCluster.tsx
index d592e7388..99c6daa56 100644
--- a/web/src/pages/studio/BrokerCluster.tsx
+++ b/web/src/pages/studio/BrokerCluster.tsx
@@ -233,7 +233,11 @@ const BrokerClusterPage = () => {
if (!active) return;
const apacheInstances = nextInstances.filter(supportsApacheRuntime);
setInstances(apacheInstances);
- setSelectedInstanceId(apacheInstances[0]?.name);
+ setSelectedInstanceId((current) =>
+ apacheInstances.some((instance) => instance.name === current)
+ ? current
+ : apacheInstances[0]?.name,
+ );
})
.catch(() => {
if (!active) return;
diff --git a/web/src/pages/studio/__tests__/BrokerClusterInstanceScope.test.tsx
b/web/src/pages/studio/__tests__/BrokerClusterInstanceScope.test.tsx
new file mode 100644
index 000000000..2dcd8188c
--- /dev/null
+++ b/web/src/pages/studio/__tests__/BrokerClusterInstanceScope.test.tsx
@@ -0,0 +1,185 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+import { beforeAll, beforeEach, describe, expect, it, vi } from 'vitest';
+import { act, render, screen, waitFor } from '@testing-library/react';
+import userEvent from '@testing-library/user-event';
+import { App } from 'antd';
+import { LangProvider, useLang } from '../../../i18n/LangContext';
+import { listClusters } from '../../../services/clusterService';
+import { listInstances } from '../../../services/instanceService';
+import type { ClusterInfo } from '../../../api/cluster';
+import type { Instance } from '../../../api/instance';
+import BrokerCluster from '../BrokerCluster';
+
+vi.mock('../../../services/clusterService', () => ({
+ listClusters: vi.fn(),
+}));
+
+vi.mock('../../../services/instanceService', () => ({
+ listInstances: vi.fn(),
+}));
+
+beforeAll(() => {
+ Object.defineProperty(window, 'matchMedia', {
+ writable: true,
+ value: vi.fn().mockImplementation((query: string) => ({
+ matches: false,
+ media: query,
+ onchange: null,
+ addListener: vi.fn(),
+ removeListener: vi.fn(),
+ addEventListener: vi.fn(),
+ removeEventListener: vi.fn(),
+ dispatchEvent: vi.fn(),
+ })),
+ });
+});
+
+vi.mock('react-router-dom', () => ({
+ useNavigate: () => vi.fn(),
+ useParams: () => ({}),
+}));
+
+const buildCluster = (id: string, brokerName: string): ClusterInfo => ({
+ id,
+ name: id,
+ nsClusterName: id,
+ type: 'V5_PROXY_CLUSTER',
+ endpoint: '10.0.0.1:9876',
+ status: 'healthy',
+ version: '5.3.0',
+ brokers: [
+ {
+ name: brokerName,
+ addr: `10.0.0.2:10911`,
+ version: '5.3.0',
+ status: 'running',
+ diskUsage: 10,
+ tpsIn: 1,
+ tpsOut: 1,
+ },
+ ],
+ proxies: [],
+ nameServers: [],
+ config: {
+ flushDiskType: 'ASYNC_FLUSH',
+ autoCreateTopicEnable: false,
+ autoCreateSubscriptionGroup: false,
+ maxMessageSize: 4194304,
+ msgTraceTopicName: 'RMQ_SYS_TRACE_TOPIC',
+ fileReservedTime: 72,
+ writeQueueNums: 8,
+ readQueueNums: 8,
+ brokerPermission: 6,
+ deleteWhen: '04',
+ },
+ topicCount: 1,
+ groupCount: 1,
+ tpsHistory: [],
+});
+
+const instanceFixture = (id: number, name: string): Instance => ({
+ id,
+ name,
+ remark: '',
+ type: 'DIRECT',
+ endpoint: '10.0.0.1:9876',
+ topicCount: 0,
+ consumerGroupCount: 0,
+ gmtCreate: '',
+ gmtModified: '',
+});
+
+const LanguageSwitch = () => {
+ const { setLang } = useLang();
+ return (
+ <button type="button" onClick={() => setLang('en')}>
+ switch-language
+ </button>
+ );
+};
+
+const renderPage = () =>
+ render(
+ <App>
+ <LangProvider>
+ <LanguageSwitch />
+ <BrokerCluster />
+ </LangProvider>
+ </App>,
+ );
+
+describe('BrokerCluster instance scope', () => {
+ beforeEach(() => {
+ vi.clearAllMocks();
+ vi.mocked(listInstances).mockResolvedValue([
+ instanceFixture(1, 'instance-1'),
+ instanceFixture(2, 'instance-2'),
+ ]);
+ vi.mocked(listClusters).mockImplementation(async (instanceId?: string) =>
+ instanceId === 'instance-2'
+ ? [buildCluster('cluster-2', 'broker-from-instance-2')]
+ : [buildCluster('cluster-1', 'broker-from-instance-1')],
+ );
+ });
+
+ it('keeps the instance the user selected when the display language changes',
async () => {
+ const user = userEvent.setup();
+ renderPage();
+
+ await screen.findByText('broker-from-instance-1');
+
+ await user.click(screen.getByRole('combobox', { name: '选择实例' }));
+ await user.click(
+ await screen.findByText('instance-2', { selector:
'.ant-select-item-option-content' }),
+ );
+ await waitFor(() =>
expect(listClusters).toHaveBeenLastCalledWith('instance-2'));
+ expect(await
screen.findByText('broker-from-instance-2')).toBeInTheDocument();
+
+ await user.click(screen.getByRole('button', { name: 'switch-language' }));
+ // Let every effect triggered by the language change settle before
asserting.
+ await act(async () => {
+ await Promise.resolve();
+ });
+
+ // The topology must stay scoped to the instance the user picked.
+ await waitFor(() =>
expect(listClusters).toHaveBeenLastCalledWith('instance-2'));
+
expect(screen.queryByText('broker-from-instance-1')).not.toBeInTheDocument();
+ expect(await
screen.findByText('broker-from-instance-2')).toBeInTheDocument();
+ }, 20_000);
+
+ it('falls back to the first instance when the selected one is gone', async
() => {
+ const user = userEvent.setup();
+ renderPage();
+
+ await screen.findByText('broker-from-instance-1');
+ await user.click(screen.getByRole('combobox', { name: '选择实例' }));
+ await user.click(
+ await screen.findByText('instance-2', { selector:
'.ant-select-item-option-content' }),
+ );
+ await waitFor(() =>
expect(listClusters).toHaveBeenLastCalledWith('instance-2'));
+
+ // The next discovery — a language change re-runs it — no longer lists
instance-2.
+ vi.mocked(listInstances).mockResolvedValue([instanceFixture(1,
'instance-1')]);
+ await user.click(screen.getByRole('button', { name: 'switch-language' }));
+
+ // Keeping a selection that no longer exists would leave the page pointed
at nothing.
+ await waitFor(() =>
expect(listClusters).toHaveBeenLastCalledWith('instance-1'));
+ expect(await
screen.findByText('broker-from-instance-1')).toBeInTheDocument();
+ }, 20_000);
+});