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 e9f5df9a9 fix: preserve unavailable dashboard topology counts (#2419)
e9f5df9a9 is described below

commit e9f5df9a9dac999126e661625e9d03a4195411f2
Author: xdz997 <[email protected]>
AuthorDate: Fri Aug 21 17:39:42 2026 +0800

    fix: preserve unavailable dashboard topology counts (#2419)
---
 .../studio/ops/dashboard/ClusterOverviewVO.java    |  3 +-
 .../studio/ops/dashboard/DashboardStatsVO.java     |  6 ++--
 .../provider/apache/RocketMQDashboardProvider.java | 36 +++++++++++++------
 .../ops/dashboard/DashboardControllerTest.java     | 32 +++++++++++++++++
 .../apache/RocketMQDashboardProviderTest.java      | 42 ++++++++++++++++++++++
 web/src/api/metrics.ts                             |  6 ++--
 .../pages/home/__tests__/DashboardPage.test.tsx    | 27 +++++++++++++-
 web/src/pages/home/dashboard.tsx                   |  9 +++--
 8 files changed, 141 insertions(+), 20 deletions(-)

diff --git 
a/server/src/main/java/org/apache/rocketmq/studio/ops/dashboard/ClusterOverviewVO.java
 
b/server/src/main/java/org/apache/rocketmq/studio/ops/dashboard/ClusterOverviewVO.java
index 716f24b7d..8be4f1b4b 100644
--- 
a/server/src/main/java/org/apache/rocketmq/studio/ops/dashboard/ClusterOverviewVO.java
+++ 
b/server/src/main/java/org/apache/rocketmq/studio/ops/dashboard/ClusterOverviewVO.java
@@ -37,7 +37,8 @@ public class ClusterOverviewVO {
     private ClusterType type;
     private ClusterStatus status;
     private int brokers;
-    private int proxies;
+    /** Null means Studio cannot discover the Proxy count through the selected 
access path. */
+    private Integer proxies;
     private int topics;
     private int groups;
     private long tpsIn;
diff --git 
a/server/src/main/java/org/apache/rocketmq/studio/ops/dashboard/DashboardStatsVO.java
 
b/server/src/main/java/org/apache/rocketmq/studio/ops/dashboard/DashboardStatsVO.java
index dcb15687a..4c9756e63 100644
--- 
a/server/src/main/java/org/apache/rocketmq/studio/ops/dashboard/DashboardStatsVO.java
+++ 
b/server/src/main/java/org/apache/rocketmq/studio/ops/dashboard/DashboardStatsVO.java
@@ -30,8 +30,10 @@ public class DashboardStatsVO {
     private int totalClusters;
     private int healthyClusters;
     private int totalBrokers;
-    private int totalProxies;
-    private int totalNameServers;
+    /** Null means Studio cannot discover the count through the selected 
access path. */
+    private Integer totalProxies;
+    /** Null means Studio cannot discover the count through the selected 
access path. */
+    private Integer totalNameServers;
     private int totalTopics;
     private int totalConsumerGroups;
     private long totalMessagesToday;
diff --git 
a/server/src/main/java/org/apache/rocketmq/studio/provider/apache/RocketMQDashboardProvider.java
 
b/server/src/main/java/org/apache/rocketmq/studio/provider/apache/RocketMQDashboardProvider.java
index 902b760c8..205983b18 100644
--- 
a/server/src/main/java/org/apache/rocketmq/studio/provider/apache/RocketMQDashboardProvider.java
+++ 
b/server/src/main/java/org/apache/rocketmq/studio/provider/apache/RocketMQDashboardProvider.java
@@ -17,6 +17,7 @@
 package org.apache.rocketmq.studio.provider.apache;
 
 import java.util.ArrayList;
+import java.util.Arrays;
 import java.util.HashMap;
 import java.util.HashSet;
 import java.util.List;
@@ -34,6 +35,7 @@ import 
org.apache.rocketmq.studio.cluster.broker.RuntimeAdminClientResolver;
 import org.apache.rocketmq.studio.common.exception.BusinessException;
 import org.apache.rocketmq.studio.common.domain.enums.ClusterStatus;
 import org.apache.rocketmq.studio.common.domain.enums.ClusterType;
+import org.apache.rocketmq.studio.common.domain.enums.InstanceType;
 import org.apache.rocketmq.studio.instance.InstanceVO;
 import org.apache.rocketmq.studio.ops.dashboard.ClusterOverviewVO;
 import org.apache.rocketmq.studio.ops.dashboard.DashboardDataVO;
@@ -69,20 +71,23 @@ public class RocketMQDashboardProvider implements 
DashboardProvider {
         String namesrvAddr = properties.getNamesrvAddr();
         if (!StringUtils.hasText(namesrvAddr)) {
             log.warn("NameServer address not configured, returning empty 
dashboard");
-            return emptyDashboard();
+            return unavailableTopologyDashboard();
         }
         return adminFactory.execute(namesrvAddr, null,
-                admin -> collectDashboardData(admin, 
ClusterType.V5_PROXY_CLUSTER));
+                admin -> collectDashboardData(admin, 
ClusterType.V5_PROXY_CLUSTER, countEndpoints(namesrvAddr)));
     }
 
     @Override
     public DashboardDataVO getDashboardData(String instanceId) {
         InstanceVO instance = 
runtimeAdminClientResolver.resolveInstance(instanceId);
+        Integer configuredNameServers = instance.getType() == 
InstanceType.DIRECT
+                ? countEndpoints(instance.getEndpoint()) : null;
         return runtimeAdminClientResolver.execute(instance,
-                admin -> collectDashboardData(admin, 
clusterTypeFor(instance)));
+                admin -> collectDashboardData(admin, clusterTypeFor(instance), 
configuredNameServers));
     }
 
-    private DashboardDataVO collectDashboardData(MQAdminExt admin, ClusterType 
clusterType) {
+    private DashboardDataVO collectDashboardData(
+            MQAdminExt admin, ClusterType clusterType, Integer 
configuredNameServers) {
         int totalClusters = 0;
         int totalBrokers = 0;
         int totalTopics = 0;
@@ -286,7 +291,7 @@ public class RocketMQDashboardProvider implements 
DashboardProvider {
                         .type(clusterType)
                         .status(runtimeMetricsUnavailable ? 
ClusterStatus.warning : ClusterStatus.healthy)
                         .brokers(clusterBrokers)
-                        .proxies(0)
+                        .proxies(clusterType == ClusterType.V4_DIRECT ? 0 : 
null)
                         .topics(topicsByCluster.getOrDefault(clusterName, 
Set.of()).size())
                         .groups(groupsByCluster.getOrDefault(clusterName, 
Set.of()).size())
                         .tpsIn(clusterTpsIn)
@@ -314,8 +319,8 @@ public class RocketMQDashboardProvider implements 
DashboardProvider {
                 .totalClusters(totalClusters)
                 .healthyClusters(healthyClusters)
                 .totalBrokers(totalBrokers)
-                .totalProxies(0)
-                .totalNameServers(0)
+                .totalProxies(clusterType == ClusterType.V4_DIRECT ? 0 : null)
+                .totalNameServers(configuredNameServers)
                 .totalTopics(totalTopics)
                 .totalConsumerGroups(totalGroups)
                 .totalMessagesToday(messagesToday)
@@ -338,13 +343,13 @@ public class RocketMQDashboardProvider implements 
DashboardProvider {
         };
     }
 
-    private DashboardDataVO emptyDashboard() {
+    private DashboardDataVO unavailableTopologyDashboard() {
         DashboardStatsVO stats = DashboardStatsVO.builder()
                 .totalClusters(0)
                 .healthyClusters(0)
                 .totalBrokers(0)
-                .totalProxies(0)
-                .totalNameServers(0)
+                .totalProxies(null)
+                .totalNameServers(null)
                 .totalTopics(0)
                 .totalConsumerGroups(0)
                 .totalMessagesToday(0)
@@ -358,6 +363,17 @@ public class RocketMQDashboardProvider implements 
DashboardProvider {
                 .build();
     }
 
+    private int countEndpoints(String endpoint) {
+        if (!StringUtils.hasText(endpoint)) {
+            return 0;
+        }
+        return (int) Arrays.stream(endpoint.split("[;,]"))
+                .map(String::trim)
+                .filter(address -> !address.isEmpty())
+                .distinct()
+                .count();
+    }
+
     /**
      * Parse TPS value from RocketMQ runtime stats format: "10minAvg 1minAvg 
10secAvg"
      * Returns the 1-minute average (second value) as a long.
diff --git 
a/server/src/test/java/org/apache/rocketmq/studio/ops/dashboard/DashboardControllerTest.java
 
b/server/src/test/java/org/apache/rocketmq/studio/ops/dashboard/DashboardControllerTest.java
index 81d35b26d..614ece430 100644
--- 
a/server/src/test/java/org/apache/rocketmq/studio/ops/dashboard/DashboardControllerTest.java
+++ 
b/server/src/test/java/org/apache/rocketmq/studio/ops/dashboard/DashboardControllerTest.java
@@ -90,6 +90,8 @@ class DashboardControllerTest {
                 .andExpect(jsonPath("$.data.stats.totalClusters").value(2))
                 .andExpect(jsonPath("$.data.stats.healthyClusters").value(2))
                 .andExpect(jsonPath("$.data.stats.totalBrokers").value(6))
+                .andExpect(jsonPath("$.data.stats.totalProxies").value(2))
+                .andExpect(jsonPath("$.data.stats.totalNameServers").value(4))
                 .andExpect(jsonPath("$.data.stats.totalTopics").value(80))
                 
.andExpect(jsonPath("$.data.stats.messagesPerSecond").value(250))
                 .andExpect(jsonPath("$.data.clusters").isArray())
@@ -101,6 +103,36 @@ class DashboardControllerTest {
         verify(dashboardService).getDashboard(isNull());
     }
 
+    @Test
+    void getDashboardShouldPreserveUnavailableTopologyCounts() throws 
Exception {
+        DashboardStatsVO stats = DashboardStatsVO.builder()
+                .totalClusters(1)
+                .healthyClusters(1)
+                .totalBrokers(2)
+                .totalProxies(null)
+                .totalNameServers(null)
+                .build();
+        ClusterOverviewVO cluster = ClusterOverviewVO.builder()
+                .id("proxy-cluster")
+                .name("proxy-cluster")
+                .type(ClusterType.V5_PROXY_CLUSTER)
+                .status(ClusterStatus.healthy)
+                .brokers(2)
+                .proxies(null)
+                .build();
+        DashboardDataVO data = DashboardDataVO.builder()
+                .stats(stats)
+                .clusters(List.of(cluster))
+                .build();
+        when(dashboardService.getDashboard(isNull())).thenReturn(data);
+
+        mockMvc.perform(get("/api/dashboard"))
+                .andExpect(status().isOk())
+                
.andExpect(jsonPath("$.data.stats.totalProxies").doesNotExist())
+                
.andExpect(jsonPath("$.data.stats.totalNameServers").doesNotExist())
+                
.andExpect(jsonPath("$.data.clusters[0].proxies").doesNotExist());
+    }
+
     @Test
     void getDashboardShouldReturnEmptyClustersWhenNoneExist() throws Exception 
{
         DashboardDataVO data = DashboardDataVO.builder()
diff --git 
a/server/src/test/java/org/apache/rocketmq/studio/provider/apache/RocketMQDashboardProviderTest.java
 
b/server/src/test/java/org/apache/rocketmq/studio/provider/apache/RocketMQDashboardProviderTest.java
index 6f5b4d516..d00dc6fa1 100644
--- 
a/server/src/test/java/org/apache/rocketmq/studio/provider/apache/RocketMQDashboardProviderTest.java
+++ 
b/server/src/test/java/org/apache/rocketmq/studio/provider/apache/RocketMQDashboardProviderTest.java
@@ -377,6 +377,9 @@ class RocketMQDashboardProviderTest {
 
         assertThat(dashboard.getClusters()).hasSize(1);
         
assertThat(dashboard.getClusters().get(0).getType()).isEqualTo(ClusterType.V4_DIRECT);
+        assertThat(dashboard.getClusters().get(0).getProxies()).isZero();
+        assertThat(dashboard.getStats().getTotalProxies()).isZero();
+        assertThat(dashboard.getStats().getTotalNameServers()).isEqualTo(1);
         verify(resolver).execute(eq(instance), any());
     }
 
@@ -401,6 +404,45 @@ class RocketMQDashboardProviderTest {
         assertThat(dashboard.getClusters()).singleElement()
                 .extracting(cluster -> cluster.getType())
                 .isEqualTo(ClusterType.V5_PROXY_LOCAL);
+        assertThat(dashboard.getClusters().get(0).getProxies()).isNull();
+        assertThat(dashboard.getStats().getTotalProxies()).isNull();
+        assertThat(dashboard.getStats().getTotalNameServers()).isNull();
+    }
+
+    @Test
+    void dashboardShouldCountConfiguredDirectNameServerEndpoints() throws 
Exception {
+        DefaultMQAdminExt adminExt = mock(DefaultMQAdminExt.class);
+        RuntimeAdminClientResolver resolver = 
mock(RuntimeAdminClientResolver.class);
+        InstanceVO instance = InstanceVO.builder()
+                .type(InstanceType.DIRECT)
+                .endpoint(" ns-a:9876 ; ns-b:9876,ns-a:9876 ;; ")
+                .build();
+        instance.setId(1L);
+        when(resolver.resolveInstance("instance-direct")).thenReturn(instance);
+        when(resolver.execute(eq(instance), any())).thenAnswer(invocation ->
+                
invocation.<MqAdminExtFactory.AdminAction<DashboardDataVO>>getArgument(1).apply(adminExt));
+        when(adminExt.examineBrokerClusterInfo()).thenReturn(clusterInfo());
+        when(adminExt.fetchAllTopicList()).thenReturn(topicList());
+        
when(adminExt.fetchBrokerRuntimeStats("10.0.0.11:10911")).thenReturn(runtimeStats());
+
+        DashboardDataVO dashboard = newProvider(adminExt, 
resolver).getDashboardData("instance-direct");
+
+        assertThat(dashboard.getStats().getTotalNameServers()).isEqualTo(2);
+    }
+
+    @Test
+    void dashboardShouldReportUnconfiguredLegacyTopologyAsUnavailable() {
+        RocketMQProperties properties = new RocketMQProperties();
+        properties.setNamesrvAddr(" ");
+        RocketMQDashboardProvider provider = new RocketMQDashboardProvider(
+                mock(MqAdminExtFactory.class), properties, 
mock(RuntimeAdminClientResolver.class));
+
+        DashboardDataVO dashboard = provider.getDashboardData();
+
+        assertThat(dashboard.getStats().getTotalClusters()).isZero();
+        assertThat(dashboard.getStats().getTotalProxies()).isNull();
+        assertThat(dashboard.getStats().getTotalNameServers()).isNull();
+        assertThat(dashboard.getClusters()).isEmpty();
     }
 
     @Test
diff --git a/web/src/api/metrics.ts b/web/src/api/metrics.ts
index 211cef07c..fb1144b86 100644
--- a/web/src/api/metrics.ts
+++ b/web/src/api/metrics.ts
@@ -10,8 +10,8 @@ export interface DashboardStats {
   totalClusters: number;
   healthyClusters: number;
   totalBrokers: number;
-  totalProxies: number;
-  totalNameServers: number;
+  totalProxies: number | null;
+  totalNameServers: number | null;
   totalTopics: number;
   totalConsumerGroups: number;
   totalMessagesToday: number;
@@ -26,7 +26,7 @@ export interface ClusterOverview {
   type: string;
   status: string;
   brokers: number;
-  proxies: number;
+  proxies: number | null;
   topics: number;
   groups: number;
   tpsIn: number;
diff --git a/web/src/pages/home/__tests__/DashboardPage.test.tsx 
b/web/src/pages/home/__tests__/DashboardPage.test.tsx
index c63045c15..898eb9f20 100644
--- a/web/src/pages/home/__tests__/DashboardPage.test.tsx
+++ b/web/src/pages/home/__tests__/DashboardPage.test.tsx
@@ -6,7 +6,7 @@
  */
 
 import { App } from 'antd';
-import { render, screen, waitFor } from '@testing-library/react';
+import { render, screen, waitFor, within } from '@testing-library/react';
 import userEvent from '@testing-library/user-event';
 import type React from 'react';
 import { MemoryRouter, useLocation } from 'react-router-dom';
@@ -52,6 +52,19 @@ const dashboard = (name: string): DashboardData => ({
   ],
 });
 
+const unavailableTopologyDashboard = (): DashboardData => ({
+  ...dashboard('proxy-cluster'),
+  stats: {
+    ...dashboard('proxy-cluster').stats,
+    totalProxies: null,
+    totalNameServers: null,
+  },
+  clusters: dashboard('proxy-cluster').clusters.map((cluster) => ({
+    ...cluster,
+    proxies: null,
+  })),
+});
+
 const deferred = <T,>() => {
   let resolve!: (value: T) => void;
   const promise = new Promise<T>((promiseResolve) => {
@@ -119,6 +132,18 @@ beforeEach(() => {
 });
 
 describe('DashboardPage', () => {
+  it('renders unavailable Proxy topology counts as N/A instead of zero', async 
() => {
+    
vi.mocked(dashboardService.getDashboard).mockResolvedValue(unavailableTopologyDashboard());
+    renderWithProviders(<DashboardPage />);
+
+    await screen.findByText('proxy-cluster');
+    expect(await screen.findByText(/1 Brokers · N\/A 
Proxy/u)).toBeInTheDocument();
+    expect(screen.queryByText('0 Proxy')).not.toBeInTheDocument();
+    const row = screen.getByText('proxy-cluster').closest('tr');
+    expect(row).not.toBeNull();
+    expect(within(row as 
HTMLElement).getAllByText('N/A').length).toBeGreaterThanOrEqual(1);
+  });
+
   it('does not show dashboard data from the previous instance while loading a 
new selection', async () => {
     const instanceA = deferred<DashboardData>();
     vi.mocked(dashboardService.getDashboard)
diff --git a/web/src/pages/home/dashboard.tsx b/web/src/pages/home/dashboard.tsx
index 2965e26e6..24c36c25b 100644
--- a/web/src/pages/home/dashboard.tsx
+++ b/web/src/pages/home/dashboard.tsx
@@ -29,6 +29,9 @@ import { useLang } from '../../i18n/LangContext';
 
 const { Text } = Typography;
 
+const renderTopologyCount = (value: number | null) =>
+  value === null ? 'N/A' : value.toLocaleString();
+
 const DashboardPage = () => {
   const navigate = useNavigate();
   const { t } = useLang();
@@ -144,7 +147,7 @@ const DashboardPage = () => {
       icon: <ClusterOutlined style={{ fontSize: 22, color: '#52c41a' }} />,
       color: '#52c41a',
       suffix: '',
-      detail: `${stats.totalBrokers} Brokers · ${stats.totalProxies} Proxy`,
+      detail: `${stats.totalBrokers} Brokers · 
${renderTopologyCount(stats.totalProxies)} Proxy`,
     },
     {
       title: t('dashboard.topics'),
@@ -209,7 +212,7 @@ const DashboardPage = () => {
       key: 'brokers',
       width: 80,
       align: 'center' as const,
-      render: (v: number) => Math.max(0, v),
+      render: renderTopologyCount,
     },
     {
       title: t('dashboard.proxy'),
@@ -217,7 +220,7 @@ const DashboardPage = () => {
       key: 'proxies',
       width: 80,
       align: 'center' as const,
-      render: (v: number) => Math.max(0, v),
+      render: renderTopologyCount,
     },
     {
       title: t('dashboard.topic'),

Reply via email to