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'),