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 9371e2a0 fix: scope NameServer config drift by selected instance
(#1477)
9371e2a0 is described below
commit 9371e2a0f02941798ff7d9f60d3afdd7980784bc
Author: aias00 <[email protected]>
AuthorDate: Tue Aug 11 15:37:05 2026 +0800
fix: scope NameServer config drift by selected instance (#1477)
---
.../nameserver/NameServerConfigDiffService.java | 15 ++++-
.../cluster/nameserver/NameServerController.java | 5 +-
.../NameServerConfigDiffServiceTest.java | 18 ++++++
.../nameserver/NameServerControllerTest.java | 11 ++--
web/src/api/cluster.test.ts | 6 +-
web/src/api/cluster.ts | 4 +-
web/src/i18n/translations.ts | 1 +
.../__tests__/NameServerConfigDriftPage.test.tsx | 16 +++++-
web/src/pages/ops/nameServerConfigDrift.tsx | 66 ++++++++++++++++++----
web/src/services/clusterService.ts | 3 +-
10 files changed, 121 insertions(+), 24 deletions(-)
diff --git
a/server/src/main/java/org/apache/rocketmq/studio/cluster/nameserver/NameServerConfigDiffService.java
b/server/src/main/java/org/apache/rocketmq/studio/cluster/nameserver/NameServerConfigDiffService.java
index bf23212d..e05028c1 100644
---
a/server/src/main/java/org/apache/rocketmq/studio/cluster/nameserver/NameServerConfigDiffService.java
+++
b/server/src/main/java/org/apache/rocketmq/studio/cluster/nameserver/NameServerConfigDiffService.java
@@ -73,7 +73,16 @@ public class NameServerConfigDiffService {
public NameServerConfigDiffVO compare(String clusterId) {
String normalizedClusterId = requireClusterId(clusterId);
- ClusterVO cluster = clusterService.getCluster(normalizedClusterId);
+ return compare(normalizedClusterId,
clusterService.getCluster(normalizedClusterId));
+ }
+
+ public NameServerConfigDiffVO compare(String clusterId, String instanceId)
{
+ String normalizedClusterId = requireClusterId(clusterId);
+ return compare(normalizedClusterId,
+ clusterService.getCluster(normalizedClusterId,
normalizeInstanceId(instanceId)));
+ }
+
+ private NameServerConfigDiffVO compare(String normalizedClusterId,
ClusterVO cluster) {
List<String> addresses = collectNameServerAddresses(cluster);
if (addresses.isEmpty()) {
throw new BusinessException(409,
@@ -202,4 +211,8 @@ public class NameServerConfigDiffService {
}
return clusterId.trim();
}
+
+ private String normalizeInstanceId(String instanceId) {
+ return instanceId == null || instanceId.isBlank() ? null :
instanceId.trim();
+ }
}
diff --git
a/server/src/main/java/org/apache/rocketmq/studio/cluster/nameserver/NameServerController.java
b/server/src/main/java/org/apache/rocketmq/studio/cluster/nameserver/NameServerController.java
index 15c0ee00..2c284a56 100644
---
a/server/src/main/java/org/apache/rocketmq/studio/cluster/nameserver/NameServerController.java
+++
b/server/src/main/java/org/apache/rocketmq/studio/cluster/nameserver/NameServerController.java
@@ -39,8 +39,9 @@ public class NameServerController {
@GetMapping("/config-diff")
public Result<NameServerConfigDiffVO> compareConfiguration(
- @RequestParam(required = false) String clusterId) {
- return Result.ok(configDiffService.compare(clusterId));
+ @RequestParam(required = false) String clusterId,
+ @RequestParam(required = false) String instanceId) {
+ return Result.ok(configDiffService.compare(clusterId, instanceId));
}
@PostMapping("/create")
diff --git
a/server/src/test/java/org/apache/rocketmq/studio/cluster/nameserver/NameServerConfigDiffServiceTest.java
b/server/src/test/java/org/apache/rocketmq/studio/cluster/nameserver/NameServerConfigDiffServiceTest.java
index 7637eb56..c660782b 100644
---
a/server/src/test/java/org/apache/rocketmq/studio/cluster/nameserver/NameServerConfigDiffServiceTest.java
+++
b/server/src/test/java/org/apache/rocketmq/studio/cluster/nameserver/NameServerConfigDiffServiceTest.java
@@ -38,6 +38,7 @@ import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.anyString;
import static org.mockito.ArgumentMatchers.isNull;
import static org.mockito.Mockito.when;
+import static org.mockito.Mockito.verify;
@ExtendWith(MockitoExtension.class)
class NameServerConfigDiffServiceTest {
@@ -108,6 +109,23 @@ class NameServerConfigDiffServiceTest {
tuple("ns-b:9876", true));
}
+ @Test
+ void compareShouldResolveClusterThroughSelectedInstance() throws Exception
{
+ stubAdminFactory();
+ when(clusterService.getCluster("cluster-a",
"instance-a")).thenReturn(cluster(
+ "ns-a:9876;ns-b:9876",
+ List.of(nameServer("ns-a:9876"), nameServer("ns-b:9876"))));
+ when(admin.getNameServerConfig(List.of("ns-a:9876")))
+ .thenReturn(Map.of("ns-a:9876", properties("listenPort",
"9876")));
+ when(admin.getNameServerConfig(List.of("ns-b:9876")))
+ .thenReturn(Map.of("ns-b:9876", properties("listenPort",
"9876")));
+
+ NameServerConfigDiffVO result = service.compare(" cluster-a ", "
instance-a ");
+
+ assertThat(result.isComplete()).isTrue();
+ verify(clusterService).getCluster("cluster-a", "instance-a");
+ }
+
@Test
void compareShouldExposeChangedAndMissingSafeValues() throws Exception {
stubAdminFactory();
diff --git
a/server/src/test/java/org/apache/rocketmq/studio/cluster/nameserver/NameServerControllerTest.java
b/server/src/test/java/org/apache/rocketmq/studio/cluster/nameserver/NameServerControllerTest.java
index d0821658..0a0a000a 100644
---
a/server/src/test/java/org/apache/rocketmq/studio/cluster/nameserver/NameServerControllerTest.java
+++
b/server/src/test/java/org/apache/rocketmq/studio/cluster/nameserver/NameServerControllerTest.java
@@ -64,21 +64,22 @@ class NameServerControllerTest {
.nodes(java.util.List.of())
.differences(java.util.List.of())
.build();
- when(configDiffService.compare("cluster-1")).thenReturn(result);
+ when(configDiffService.compare("cluster-1",
"instance-1")).thenReturn(result);
mockMvc.perform(get("/api/nameservers/config-diff")
- .param("clusterId", "cluster-1"))
+ .param("clusterId", "cluster-1")
+ .param("instanceId", "instance-1"))
.andExpect(status().isOk())
.andExpect(jsonPath("$.code").value(200))
.andExpect(jsonPath("$.data.cluster").value("cluster-1"))
.andExpect(jsonPath("$.data.driftDetected").value(true));
- verify(configDiffService).compare("cluster-1");
+ verify(configDiffService).compare("cluster-1", "instance-1");
}
@Test
void compareConfigurationShouldRejectMissingClusterId() throws Exception {
- when(configDiffService.compare(null)).thenThrow(
+ when(configDiffService.compare(null, null)).thenThrow(
new
org.apache.rocketmq.studio.common.exception.BusinessException(
400, "cluster is required"));
@@ -87,7 +88,7 @@ class NameServerControllerTest {
.andExpect(jsonPath("$.code").value(400))
.andExpect(jsonPath("$.message").value("cluster is required"));
- verify(configDiffService).compare(null);
+ verify(configDiffService).compare(null, null);
}
@Test
diff --git a/web/src/api/cluster.test.ts b/web/src/api/cluster.test.ts
index 1500b8d9..151faa23 100644
--- a/web/src/api/cluster.test.ts
+++ b/web/src/api/cluster.test.ts
@@ -195,12 +195,14 @@ describe('K8s certificate API', () => {
},
],
};
- mock.onGet('/nameservers/config-diff', { params: { clusterId: 'cluster-1'
} }).reply(200, {
+ mock.onGet('/nameservers/config-diff', {
+ params: { clusterId: 'cluster-1', instanceId: 'instance-1' },
+ }).reply(200, {
code: 200,
data: result,
});
- await
expect(getNameServerConfigDiff('cluster-1')).resolves.toEqual(result);
+ await expect(getNameServerConfigDiff('cluster-1',
'instance-1')).resolves.toEqual(result);
});
it('sends the proxy restart target', async () => {
diff --git a/web/src/api/cluster.ts b/web/src/api/cluster.ts
index b7851da9..3b189b83 100644
--- a/web/src/api/cluster.ts
+++ b/web/src/api/cluster.ts
@@ -202,9 +202,9 @@ export async function updateNameServer(data: {
await client.post('/nameservers/update', data);
}
-export async function getNameServerConfigDiff(clusterId: string) {
+export async function getNameServerConfigDiff(clusterId: string, instanceId?:
string) {
const res = await client.get<{ data: NameServerConfigDiffResult
}>('/nameservers/config-diff', {
- params: { clusterId },
+ params: { clusterId, ...(instanceId ? { instanceId } : {}) },
});
return res.data.data;
}
diff --git a/web/src/i18n/translations.ts b/web/src/i18n/translations.ts
index c0b62711..35b7308b 100644
--- a/web/src/i18n/translations.ts
+++ b/web/src/i18n/translations.ts
@@ -1419,6 +1419,7 @@ const translations: Record<string, Record<Lang, string>>
= {
'common.date': { zh: '日期', en: 'Date' },
'common.begin': { zh: '开始', en: 'Begin' },
'common.end': { zh: '结束', en: 'End' },
+ 'common.selectInstance': { zh: '选择实例', en: 'Select Instance' },
'common.selectProxy': { zh: '选择代理', en: 'Select Proxy' },
'common.enableProxy': { zh: '启用代理', en: 'Enable Proxy' },
'common.proxyDisabled': { zh: '代理禁用', en: 'Proxy Disabled' },
diff --git a/web/src/pages/ops/__tests__/NameServerConfigDriftPage.test.tsx
b/web/src/pages/ops/__tests__/NameServerConfigDriftPage.test.tsx
index f924694a..d270461d 100644
--- a/web/src/pages/ops/__tests__/NameServerConfigDriftPage.test.tsx
+++ b/web/src/pages/ops/__tests__/NameServerConfigDriftPage.test.tsx
@@ -21,8 +21,10 @@ import { render, screen, waitFor } from
'@testing-library/react';
import userEvent from '@testing-library/user-event';
import { App } from 'antd';
import { type ClusterInfo, type NameServerConfigDiffResult } from
'../../../api/cluster';
+import type { Instance } from '../../../api/instance';
import { LangProvider } from '../../../i18n/LangContext';
import { getNameServerConfigDiff, listClusters } from
'../../../services/clusterService';
+import { listInstances } from '../../../services/instanceService';
import NameServerConfigDriftPage from '../nameServerConfigDrift';
vi.mock('../../../services/clusterService', () => ({
@@ -30,6 +32,10 @@ vi.mock('../../../services/clusterService', () => ({
listClusters: vi.fn(),
}));
+vi.mock('../../../services/instanceService', () => ({
+ listInstances: vi.fn(),
+}));
+
const createObjectURL = vi.fn(() => 'blob:nameserver-config-drift');
const revokeObjectURL = vi.fn();
@@ -58,6 +64,12 @@ const cluster = {
name: 'Production',
} as ClusterInfo;
+const instance = {
+ id: 'instance-a',
+ name: 'Production instance',
+ vendor: 'APACHE',
+} as Instance;
+
const driftResult: NameServerConfigDiffResult = {
cluster: 'cluster-a',
complete: true,
@@ -90,6 +102,7 @@ const renderWithProviders = (ui: ReactElement) =>
describe('NameServerConfigDriftPage', () => {
beforeEach(() => {
vi.clearAllMocks();
+ vi.mocked(listInstances).mockResolvedValue([instance]);
vi.mocked(listClusters).mockResolvedValue([cluster]);
vi.mocked(getNameServerConfigDiff).mockResolvedValue(driftResult);
});
@@ -98,7 +111,8 @@ describe('NameServerConfigDriftPage', () => {
renderWithProviders(<NameServerConfigDriftPage />);
await waitFor(() => {
- expect(getNameServerConfigDiff).toHaveBeenCalledWith('cluster-a');
+ expect(listClusters).toHaveBeenCalledWith('instance-a');
+ expect(getNameServerConfigDiff).toHaveBeenCalledWith('cluster-a',
'instance-a');
});
expect(await screen.findByText('检测到配置漂移')).toBeInTheDocument();
expect(screen.getByText('listenPort')).toBeInTheDocument();
diff --git a/web/src/pages/ops/nameServerConfigDrift.tsx
b/web/src/pages/ops/nameServerConfigDrift.tsx
index 2897e754..4e19c61f 100644
--- a/web/src/pages/ops/nameServerConfigDrift.tsx
+++ b/web/src/pages/ops/nameServerConfigDrift.tsx
@@ -27,6 +27,8 @@ import {
import PageHeader from '../../components/PageHeader';
import { useLang } from '../../i18n/LangContext';
import { getNameServerConfigDiff, listClusters } from
'../../services/clusterService';
+import { listInstances } from '../../services/instanceService';
+import type { Instance } from '../../api/instance';
const { Text, Title } = Typography;
@@ -34,6 +36,8 @@ const NameServerConfigDriftPage = () => {
const { t } = useLang();
const { message } = App.useApp();
const requestSequence = useRef(0);
+ const [instances, setInstances] = useState<Instance[]>([]);
+ const [selectedInstanceId, setSelectedInstanceId] = useState<string>();
const [clusters, setClusters] = useState<ClusterInfo[]>([]);
const [selectedClusterId, setSelectedClusterId] = useState<string>();
const [clustersLoading, setClustersLoading] = useState(true);
@@ -41,11 +45,11 @@ const NameServerConfigDriftPage = () => {
const [result, setResult] = useState<NameServerConfigDiffResult>();
const runCheck = useCallback(
- async (clusterId: string) => {
+ async (clusterId: string, instanceId: string) => {
const sequence = ++requestSequence.current;
setChecking(true);
try {
- const nextResult = await getNameServerConfigDiff(clusterId);
+ const nextResult = await getNameServerConfigDiff(clusterId,
instanceId);
if (sequence === requestSequence.current) setResult(nextResult);
} catch {
if (sequence === requestSequence.current) {
@@ -61,13 +65,23 @@ const NameServerConfigDriftPage = () => {
useEffect(() => {
let cancelled = false;
- void listClusters()
- .then((items) => {
+ void listInstances()
+ .then(async (items) => {
if (cancelled) return;
- setClusters(items);
- const firstClusterId = items[0]?.id;
+ const apacheInstances = items.filter((instance) => instance.vendor ===
'APACHE');
+ setInstances(apacheInstances);
+ const firstInstanceId = apacheInstances[0]?.id;
+ setSelectedInstanceId(firstInstanceId);
+ if (!firstInstanceId) {
+ setClusters([]);
+ return;
+ }
+ const clustersForInstance = await listClusters(firstInstanceId);
+ if (cancelled) return;
+ setClusters(clustersForInstance);
+ const firstClusterId = clustersForInstance[0]?.id;
setSelectedClusterId(firstClusterId);
- if (firstClusterId) void runCheck(firstClusterId);
+ if (firstClusterId) void runCheck(firstClusterId, firstInstanceId);
})
.catch(() => {
if (!cancelled) message.error(t('nameServerDrift.loadClustersFailed'));
@@ -81,10 +95,31 @@ const NameServerConfigDriftPage = () => {
};
}, [message, runCheck, t]);
+ const selectInstance = async (instanceId: string) => {
+ const sequence = ++requestSequence.current;
+ setSelectedInstanceId(instanceId);
+ setSelectedClusterId(undefined);
+ setClusters([]);
+ setResult(undefined);
+ setClustersLoading(true);
+ try {
+ const nextClusters = await listClusters(instanceId);
+ if (sequence !== requestSequence.current) return;
+ setClusters(nextClusters);
+ const firstClusterId = nextClusters[0]?.id;
+ setSelectedClusterId(firstClusterId);
+ if (firstClusterId) void runCheck(firstClusterId, instanceId);
+ } catch {
+ if (sequence === requestSequence.current)
message.error(t('nameServerDrift.loadClustersFailed'));
+ } finally {
+ if (sequence === requestSequence.current) setClustersLoading(false);
+ }
+ };
+
const selectCluster = (clusterId: string) => {
setSelectedClusterId(clusterId);
setResult(undefined);
- void runCheck(clusterId);
+ if (selectedInstanceId) void runCheck(clusterId, selectedInstanceId);
};
const columns = useMemo<TableColumnsType<NameServerConfigDifference>>(() => {
@@ -158,6 +193,15 @@ const NameServerConfigDriftPage = () => {
<PageHeader title={t('nameServerDrift.title')} />
<Flex wrap gap={8} align="center" style={{ marginBottom: 20 }}>
+ <Select
+ aria-label="NameServer drift instance"
+ loading={clustersLoading}
+ value={selectedInstanceId}
+ onChange={(instanceId) => void selectInstance(instanceId)}
+ placeholder={t('common.selectInstance')}
+ options={instances.map((instance) => ({ label: instance.name, value:
instance.id }))}
+ style={{ width: 'min(100%, 280px)' }}
+ />
<Select
aria-label={t('nameServerDrift.cluster')}
loading={clustersLoading}
@@ -175,8 +219,10 @@ const NameServerConfigDriftPage = () => {
aria-label={t('nameServerDrift.refresh')}
icon={<ArrowsClockwise size={16} />}
loading={checking}
- disabled={!selectedClusterId}
- onClick={() => selectedClusterId && void
runCheck(selectedClusterId)}
+ disabled={!selectedClusterId || !selectedInstanceId}
+ onClick={() =>
+ selectedClusterId && selectedInstanceId && void
runCheck(selectedClusterId, selectedInstanceId)
+ }
/>
</Tooltip>
<Tooltip title={t('nameServerDrift.export')}>
diff --git a/web/src/services/clusterService.ts
b/web/src/services/clusterService.ts
index 65bef100..9b39aa93 100644
--- a/web/src/services/clusterService.ts
+++ b/web/src/services/clusterService.ts
@@ -70,8 +70,9 @@ export async function getCluster(id: string, instanceId?:
string): Promise<Clust
export async function getNameServerConfigDiff(
clusterId: string,
+ instanceId?: string,
): Promise<NameServerConfigDiffResult> {
- if (!isMockMode()) return clusterApi.getNameServerConfigDiff(clusterId);
+ if (!isMockMode()) return clusterApi.getNameServerConfigDiff(clusterId,
instanceId);
const cluster = getMockCluster(clusterId);
const nodes = cluster.nameServers.map((nameServer) => ({