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

Reply via email to