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 2fc9ebd6 fix: harden cluster runtime data and presentation (#950)
2fc9ebd6 is described below
commit 2fc9ebd69a390dae2adab4b5c7dc88f5502cd913
Author: aias00 <[email protected]>
AuthorDate: Tue Aug 4 03:07:16 2026 -0700
fix: harden cluster runtime data and presentation (#950)
* fix: gate demo cluster seed data
* [ISSUE #802] Disable mock mode for production builds
* fix(web): remove BrokerCluster mock fallback rows
* [ISSUE #804] Load BrokerCluster data from service
* fix: stop fabricating proxy runtime metrics
* fix: handle missing cluster addresses
* [ISSUE #790] Gate demo instance seed data
---
.../cluster/broker/ClusterRepositoryImpl.java | 8 +-
server/src/main/resources/application.yml | 2 +
.../cluster/broker/ClusterRepositoryImplTest.java | 9 +-
.../MybatisPlusInstanceRepositoryTest.java | 8 ++
web/.env.production | 6 +-
web/src/api/proxy.ts | 14 +--
.../pages/cluster/__tests__/ClusterPage.test.tsx | 32 +++++
web/src/pages/cluster/index.tsx | 34 +++---
web/src/pages/studio/BrokerCluster.tsx | 129 +--------------------
web/src/pages/studio/Proxy.tsx | 114 ++++++++++--------
.../pages/studio/__tests__/BrokerCluster.test.tsx | 31 ++---
web/src/pages/studio/__tests__/Proxy.test.tsx | 8 ++
12 files changed, 183 insertions(+), 212 deletions(-)
diff --git
a/server/src/main/java/org/apache/rocketmq/studio/cluster/broker/ClusterRepositoryImpl.java
b/server/src/main/java/org/apache/rocketmq/studio/cluster/broker/ClusterRepositoryImpl.java
index 7bdfa8e0..e4252219 100644
---
a/server/src/main/java/org/apache/rocketmq/studio/cluster/broker/ClusterRepositoryImpl.java
+++
b/server/src/main/java/org/apache/rocketmq/studio/cluster/broker/ClusterRepositoryImpl.java
@@ -25,6 +25,7 @@ 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.FlushDiskType;
import lombok.extern.slf4j.Slf4j;
+import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Repository;
import java.time.LocalDateTime;
@@ -40,8 +41,11 @@ public class ClusterRepositoryImpl implements
ClusterRepository {
private final Map<String, ClusterVO> store = new ConcurrentHashMap<>();
- public ClusterRepositoryImpl() {
- initStubData();
+ public
ClusterRepositoryImpl(@Value("${studio.cluster.seed-demo-data:false}") boolean
seedDemoData) {
+ if (seedDemoData) {
+ initStubData();
+ log.info("Initialized demo cluster data for Studio cluster
repository");
+ }
}
@Override
diff --git a/server/src/main/resources/application.yml
b/server/src/main/resources/application.yml
index ce721da0..7295d3db 100644
--- a/server/src/main/resources/application.yml
+++ b/server/src/main/resources/application.yml
@@ -34,6 +34,8 @@ studio:
- username: ${STUDIO_AUTH_ADMIN_USERNAME:}
password: ${STUDIO_AUTH_ADMIN_PASSWORD:}
admin: true
+ cluster:
+ seed-demo-data: ${STUDIO_CLUSTER_SEED_DEMO_DATA:false}
metrics:
prometheus:
base-url: ${STUDIO_METRICS_PROMETHEUS_BASE_URL:}
diff --git
a/server/src/test/java/org/apache/rocketmq/studio/cluster/broker/ClusterRepositoryImplTest.java
b/server/src/test/java/org/apache/rocketmq/studio/cluster/broker/ClusterRepositoryImplTest.java
index e805803e..ac074a44 100644
---
a/server/src/test/java/org/apache/rocketmq/studio/cluster/broker/ClusterRepositoryImplTest.java
+++
b/server/src/test/java/org/apache/rocketmq/studio/cluster/broker/ClusterRepositoryImplTest.java
@@ -26,11 +26,18 @@ class ClusterRepositoryImplTest {
@Test
void findAllShouldReturnClustersInStableNameOrder() {
- ClusterRepositoryImpl repository = new ClusterRepositoryImpl();
+ ClusterRepositoryImpl repository = new ClusterRepositoryImpl(true);
List<ClusterVO> clusters = repository.findAll();
assertThat(clusters).extracting(ClusterVO::getName)
.containsExactly("rmq-cluster-prod", "rmq-cluster-staging");
}
+
+ @Test
+ void findAllShouldBeEmptyWhenDemoDataIsDisabled() {
+ ClusterRepositoryImpl repository = new ClusterRepositoryImpl(false);
+
+ assertThat(repository.findAll()).isEmpty();
+ }
}
diff --git
a/server/src/test/java/org/apache/rocketmq/studio/instance/MybatisPlusInstanceRepositoryTest.java
b/server/src/test/java/org/apache/rocketmq/studio/instance/MybatisPlusInstanceRepositoryTest.java
index 6f71d802..eba043f8 100644
---
a/server/src/test/java/org/apache/rocketmq/studio/instance/MybatisPlusInstanceRepositoryTest.java
+++
b/server/src/test/java/org/apache/rocketmq/studio/instance/MybatisPlusInstanceRepositoryTest.java
@@ -79,6 +79,14 @@ class MybatisPlusInstanceRepositoryTest {
assertThat(proxy.getConsumerGroupCount()).isEqualTo(2);
}
+ @Test
+ void constructorShouldNotSeedDemoInstances() {
+
when(instanceMapper.selectList(any(QueryWrapper.class))).thenReturn(List.of());
+
+ assertThat(repository.findAll()).isEmpty();
+ verify(instanceMapper, never()).insert(any(RmqInstance.class));
+ }
+
@Test
void findAllShouldReturnEmptyWithoutCountQueriesWhenNoInstances() {
when(instanceMapper.selectList(any(QueryWrapper.class))).thenReturn(List.of());
diff --git a/web/.env.production b/web/.env.production
index 3e827984..b406b7c0 100644
--- a/web/.env.production
+++ b/web/.env.production
@@ -1,3 +1,3 @@
-# Backend not ready yet - use mock data
-# Set to "false" when real API is available
-VITE_USE_MOCK=true
+# Production builds should call real backend APIs by default.
+# Set to "true" only for explicit demo/mock deployments.
+VITE_USE_MOCK=false
diff --git a/web/src/api/proxy.ts b/web/src/api/proxy.ts
index 26e311cc..2ed50e67 100644
--- a/web/src/api/proxy.ts
+++ b/web/src/api/proxy.ts
@@ -27,13 +27,13 @@ export interface ProxyHomePageData {
export interface ProxyNode {
key: string;
address: string;
- status: 'healthy' | 'unhealthy' | 'warning';
- version: string;
- connections: number;
- tps: number;
- memory: number;
- cpu: number;
- uptime: string;
+ status: 'healthy' | 'unhealthy' | 'warning' | 'unknown';
+ version: string | null;
+ connections: number | null;
+ tps: number | null;
+ memory: number | null;
+ cpu: number | null;
+ uptime: string | null;
isSelected: boolean;
}
diff --git a/web/src/pages/cluster/__tests__/ClusterPage.test.tsx
b/web/src/pages/cluster/__tests__/ClusterPage.test.tsx
index 7d3aa093..b436e577 100644
--- a/web/src/pages/cluster/__tests__/ClusterPage.test.tsx
+++ b/web/src/pages/cluster/__tests__/ClusterPage.test.tsx
@@ -163,6 +163,38 @@ describe('Cluster page', () => {
expect(within(dialog).getByText('8080')).toBeInTheDocument();
});
+ it('keeps cluster tabs usable when address fields are missing', async () => {
+ const user = userEvent.setup();
+ const submitSearch = async (placeholder: string, value: string) => {
+ const input = screen.getByPlaceholderText(placeholder);
+ await user.type(input, value);
+ const searchBox = input.closest('.ant-input-search');
+ expect(searchBox).not.toBeNull();
+ const searchButton = (searchBox as
HTMLElement).querySelector('.ant-input-search-button');
+ expect(searchButton).not.toBeNull();
+ await user.click(searchButton as HTMLElement);
+ };
+ const cluster = buildCluster();
+ cluster.brokers = [{ ...cluster.brokers[0], addr: null as unknown as
string }];
+ cluster.nameServers = [{ ...cluster.nameServers[0], addr: null as unknown
as string }];
+ cluster.proxies = [{ ...cluster.proxies[0], addr: null as unknown as
string }];
+ clusterServiceMocks.listClusters.mockResolvedValue([cluster]);
+
+ renderWithProviders(<ClusterPage />);
+ expect(await screen.findByText('rocketmq-prod-0')).toBeInTheDocument();
+
+ await submitSearch('搜索 Broker 名称或地址', 'not-found');
+ expect(screen.queryByText('rocketmq-prod-0')).not.toBeInTheDocument();
+
+ await user.click(screen.getByRole('tab', { name: /NameServer 管理/ }));
+ await submitSearch('搜索地址', 'not-found');
+ expect(screen.getByPlaceholderText('搜索地址')).toHaveValue('not-found');
+
+ await user.click(screen.getByRole('tab', { name: /Proxy 管理/ }));
+ await submitSearch('搜索 Proxy 地址', 'not-found');
+ expect(screen.getByPlaceholderText('搜索 Proxy
地址')).toHaveValue('not-found');
+ });
+
it('polls the API after two seconds and renders only returned metrics',
async () => {
vi.useFakeTimers();
const randomSpy = vi.spyOn(Math, 'random');
diff --git a/web/src/pages/cluster/index.tsx b/web/src/pages/cluster/index.tsx
index 4d4e0a8e..5d873148 100644
--- a/web/src/pages/cluster/index.tsx
+++ b/web/src/pages/cluster/index.tsx
@@ -72,6 +72,11 @@ type RefreshSource = 'initial' | 'manual' | 'operation' |
'background';
type ProxyDetail = ProxyInfo & { clusterId: string; clusterName: string;
nsClusterName: string };
+const safeText = (value: string | null | undefined) => value ?? '';
+const searchText = (value: string | null | undefined) =>
safeText(value).toLowerCase();
+const compareText = (left: string | null | undefined, right: string | null |
undefined) =>
+ safeText(left).localeCompare(safeText(right));
+
// ─── Page
─────────────────────────────────────────────────────────────────────
const ClusterPage = () => {
@@ -329,14 +334,15 @@ const ClusterPage = () => {
nsClusterName: string;
cluster: ClusterInfo;
};
+ const brokerSearchText = searchText(brokerSearch);
const allBrokers: BrokerWithCluster[] = clusters.flatMap((c) =>
c.brokers
.filter((b) => {
const matchSearch =
- !brokerSearch ||
- b.name.toLowerCase().includes(brokerSearch.toLowerCase()) ||
- b.addr.toLowerCase().includes(brokerSearch.toLowerCase());
+ !brokerSearchText ||
+ searchText(b.name).includes(brokerSearchText) ||
+ searchText(b.addr).includes(brokerSearchText);
const matchNsCluster =
!brokerNsClusterFilter || c.nsClusterName ===
brokerNsClusterFilter;
return matchSearch && matchNsCluster;
@@ -419,8 +425,8 @@ const ClusterPage = () => {
key: 'addr',
width: 170,
align: 'right',
- sorter: (a, b) => a.addr.localeCompare(b.addr),
- render: (addr: string) => <span style={{ fontSize: 13 }}>{addr}</span>,
+ sorter: (a, b) => compareText(a.addr, b.addr),
+ render: (addr: string | null) => <span style={{ fontSize: 13
}}>{safeText(addr)}</span>,
},
{
title: 'TPS In',
@@ -577,10 +583,11 @@ const ClusterPage = () => {
// ─── Tab 2: NameServer 管理 (nested by cluster) ────────────────────────────
function renderNameServerTab() {
+ const nsSearchText = searchText(nsSearch);
const filteredClusters = clusters
.map((c) => {
const nameServers = c.nameServers.filter((ns) => {
- const matchSearch = !nsSearch ||
ns.addr.toLowerCase().includes(nsSearch.toLowerCase());
+ const matchSearch = !nsSearchText ||
searchText(ns.addr).includes(nsSearchText);
return matchSearch;
});
return { ...c, filteredNameServers: nameServers };
@@ -592,10 +599,10 @@ const ClusterPage = () => {
title: t('common.address'),
dataIndex: 'addr',
key: 'addr',
- sorter: (a, b) => a.addr.localeCompare(b.addr),
- render: (addr: string) => (
+ sorter: (a, b) => compareText(a.addr, b.addr),
+ render: (addr: string | null) => (
<Text code style={{ fontSize: 12 }}>
- {addr}
+ {safeText(addr)}
</Text>
),
},
@@ -750,6 +757,7 @@ const ClusterPage = () => {
function renderProxyTab() {
type ProxyRow = ProxyDetail;
+ const proxySearchText = searchText(proxySearch);
const allProxies: ProxyRow[] = clusters
.filter((c) => c.proxies.length > 0)
@@ -757,7 +765,7 @@ const ClusterPage = () => {
c.proxies
.filter((p) => {
const matchSearch =
- !proxySearch ||
p.addr.toLowerCase().includes(proxySearch.toLowerCase());
+ !proxySearchText || searchText(p.addr).includes(proxySearchText);
return matchSearch;
})
.map((p) => ({
@@ -786,10 +794,10 @@ const ClusterPage = () => {
dataIndex: 'addr',
key: 'addr',
width: 200,
- sorter: (a, b) => a.addr.localeCompare(b.addr),
- render: (addr: string) => (
+ sorter: (a, b) => compareText(a.addr, b.addr),
+ render: (addr: string | null) => (
<Text code style={{ fontSize: 12 }}>
- {addr}
+ {safeText(addr)}
</Text>
),
},
diff --git a/web/src/pages/studio/BrokerCluster.tsx
b/web/src/pages/studio/BrokerCluster.tsx
index 9e589c0d..3063def5 100644
--- a/web/src/pages/studio/BrokerCluster.tsx
+++ b/web/src/pages/studio/BrokerCluster.tsx
@@ -66,129 +66,6 @@ interface ProxyRecord {
connections: number;
}
-// ─── Mock Data (fallback when the API is unavailable) ───────────
-const mockBrokerData: BrokerRecord[] = [
- {
- key: '1',
- k8sCluster: 'prod-cn-east-1',
- brokerName: 'broker-a',
- status: 'running',
- version: '5.3.0',
- diskUsage: 62,
- address: '10.0.1.10:10911',
- tpsIn: 12580,
- tpsOut: 8340,
- },
- {
- key: '2',
- k8sCluster: 'prod-cn-east-1',
- brokerName: 'broker-b',
- status: 'readonly',
- version: '5.3.0',
- diskUsage: 89,
- address: '10.0.1.11:10911',
- tpsIn: 0,
- tpsOut: 3120,
- },
- {
- key: '3',
- k8sCluster: 'prod-cn-east-1',
- brokerName: 'broker-c',
- status: 'running',
- version: '5.2.0',
- diskUsage: 45,
- address: '10.0.1.12:10911',
- tpsIn: 9750,
- tpsOut: 6280,
- },
- {
- key: '4',
- k8sCluster: 'prod-cn-south-1',
- brokerName: 'broker-d',
- status: 'maintenance',
- version: '5.3.0',
- diskUsage: 33,
- address: '10.0.2.10:10911',
- tpsIn: 0,
- tpsOut: 0,
- },
- {
- key: '5',
- k8sCluster: 'prod-cn-south-1',
- brokerName: 'broker-e',
- status: 'running',
- version: '5.3.0',
- diskUsage: 51,
- address: '10.0.2.11:10911',
- tpsIn: 7890,
- tpsOut: 5430,
- },
- {
- key: '6',
- k8sCluster: 'staging-cn-east-1',
- brokerName: 'broker-staging-a',
- status: 'running',
- version: '5.3.1',
- diskUsage: 28,
- address: '10.0.10.10:10911',
- tpsIn: 1230,
- tpsOut: 980,
- },
-];
-
-const mockNameServerData: NameServerRecord[] = [
- {
- key: '1',
- k8sCluster: 'prod-cn-east-1',
- name: 'nameserver-a',
- status: 'running',
- version: '5.3.0',
- address: '10.0.1.20:9876',
- connections: 156,
- },
- {
- key: '2',
- k8sCluster: 'prod-cn-east-1',
- name: 'nameserver-b',
- status: 'running',
- version: '5.3.0',
- address: '10.0.1.21:9876',
- connections: 148,
- },
- {
- key: '3',
- k8sCluster: 'prod-cn-south-1',
- name: 'nameserver-c',
- status: 'running',
- version: '5.3.0',
- address: '10.0.2.20:9876',
- connections: 92,
- },
-];
-
-const mockProxyData: ProxyRecord[] = [
- {
- key: '1',
- k8sCluster: 'prod-cn-east-1',
- name: 'proxy-a',
- status: 'running',
- version: '5.3.0',
- address: '10.0.1.30:8080',
- grpcPort: '10.0.1.30:8081',
- connections: 2340,
- },
- {
- key: '2',
- k8sCluster: 'prod-cn-south-1',
- name: 'proxy-b',
- status: 'running',
- version: '5.3.0',
- address: '10.0.2.30:8080',
- grpcPort: '10.0.2.30:8081',
- connections: 1560,
- },
-];
-
// ─── Helpers ────────────────────────────────────────────────────
const normalizeStatus = (status: string): NodeStatus => {
const value = (status || '').toLowerCase();
@@ -260,9 +137,9 @@ const BrokerClusterPage = () => {
const [autoRefresh, setAutoRefresh] = useState(false);
const [activeTab, setActiveTab] = useState('broker');
const [loading, setLoading] = useState(false);
- const [brokerData, setBrokerData] = useState<BrokerRecord[]>(mockBrokerData);
- const [nameServerData, setNameServerData] =
useState<NameServerRecord[]>(mockNameServerData);
- const [proxyData, setProxyData] = useState<ProxyRecord[]>(mockProxyData);
+ const [brokerData, setBrokerData] = useState<BrokerRecord[]>([]);
+ const [nameServerData, setNameServerData] = useState<NameServerRecord[]>([]);
+ const [proxyData, setProxyData] = useState<ProxyRecord[]>([]);
const { t } = useLang();
const { message } = App.useApp();
diff --git a/web/src/pages/studio/Proxy.tsx b/web/src/pages/studio/Proxy.tsx
index 40812baa..590498cb 100644
--- a/web/src/pages/studio/Proxy.tsx
+++ b/web/src/pages/studio/Proxy.tsx
@@ -34,6 +34,7 @@ import {
Tooltip,
Popconfirm,
App,
+ Typography,
} from 'antd';
import type { ColumnsType } from 'antd/es/table';
import {
@@ -50,6 +51,8 @@ import PageHeader from '../../components/PageHeader';
import { useLang } from '../../i18n/LangContext';
import { queryProxyHomePage, addProxyAddr, removeProxyAddr, type ProxyNode }
from '../../api/proxy';
+const { Text } = Typography;
+
const ProxyPage: React.FC = () => {
const { t } = useLang();
const { message } = App.useApp();
@@ -63,9 +66,9 @@ const ProxyPage: React.FC = () => {
const [clusterStats, setClusterStats] = useState({
totalNodes: 0,
- healthyNodes: 0,
- totalConnections: 0,
- totalTPS: 0,
+ healthyNodes: null as number | null,
+ totalConnections: null as number | null,
+ totalTPS: null as number | null,
});
const initialized = useRef<boolean | null>(null);
@@ -81,25 +84,22 @@ const ProxyPage: React.FC = () => {
const nodes: ProxyNode[] = (proxyAddrList || []).map((addr) => ({
key: addr,
address: addr,
- status: 'healthy' as const,
- version: '5.3.0',
- connections: Math.floor(Math.random() * 1000) + 100,
- tps: Math.floor(Math.random() * 5000) + 1000,
- memory: Math.floor(Math.random() * 60) + 20,
- cpu: Math.floor(Math.random() * 50) + 10,
- uptime: `${Math.floor(Math.random() * 30) + 1}d`,
+ status: 'unknown' as const,
+ version: null,
+ connections: null,
+ tps: null,
+ memory: null,
+ cpu: null,
+ uptime: null,
isSelected: addr === currentProxyAddr,
}));
setProxyNodes(nodes);
- const healthyCount = nodes.filter((n) => n.status === 'healthy').length;
- const totalConn = nodes.reduce((sum, n) => sum + n.connections, 0);
- const totalTPS = nodes.reduce((sum, n) => sum + n.tps, 0);
setClusterStats({
totalNodes: nodes.length,
- healthyNodes: healthyCount,
- totalConnections: totalConn,
- totalTPS,
+ healthyNodes: null,
+ totalConnections: null,
+ totalTPS: null,
});
if (currentProxyAddr) {
@@ -183,8 +183,13 @@ const ProxyPage: React.FC = () => {
icon: <Warning size={12} weight="fill" />,
label: t('proxy.warning'),
},
+ unknown: {
+ color: 'default',
+ icon: null,
+ label: t('common.na'),
+ },
};
- const cfg = map[status] || map.healthy;
+ const cfg = map[status] || map.unknown;
return (
<Tag color={cfg.color} icon={cfg.icon}>
{cfg.label}
@@ -192,6 +197,14 @@ const ProxyPage: React.FC = () => {
);
};
+ const renderUnavailable = () => <Text
type="secondary">{t('common.na')}</Text>;
+
+ const renderNumberMetric = (value: number | null) =>
+ value == null ? renderUnavailable() : value.toLocaleString();
+
+ const compareNullable = (left: number | null, right: number | null) =>
+ (left ?? Number.NEGATIVE_INFINITY) - (right ?? Number.NEGATIVE_INFINITY);
+
// ─── Columns ─────────────────────────────────────────────────
const columns: ColumnsType<ProxyNode> = [
@@ -216,53 +229,61 @@ const ProxyPage: React.FC = () => {
title: t('proxy.version'),
dataIndex: 'version',
key: 'version',
+ render: (value: string | null) => value || renderUnavailable(),
},
{
title: t('proxy.connections'),
dataIndex: 'connections',
key: 'connections',
- render: (val: number) => val.toLocaleString(),
- sorter: (a, b) => a.connections - b.connections,
+ render: renderNumberMetric,
+ sorter: (a, b) => compareNullable(a.connections, b.connections),
},
{
title: 'TPS',
dataIndex: 'tps',
key: 'tps',
- render: (val: number) => val.toLocaleString(),
- sorter: (a, b) => a.tps - b.tps,
+ render: renderNumberMetric,
+ sorter: (a, b) => compareNullable(a.tps, b.tps),
},
{
title: t('proxy.memory'),
dataIndex: 'memory',
key: 'memory',
- render: (val: number) => (
- <Progress
- percent={val}
- size="small"
- status={val > 80 ? 'exception' : 'normal'}
- style={{ width: 100 }}
- />
- ),
- sorter: (a, b) => a.memory - b.memory,
+ render: (val: number | null) =>
+ val == null ? (
+ renderUnavailable()
+ ) : (
+ <Progress
+ percent={val}
+ size="small"
+ status={val > 80 ? 'exception' : 'normal'}
+ style={{ width: 100 }}
+ />
+ ),
+ sorter: (a, b) => compareNullable(a.memory, b.memory),
},
{
title: 'CPU',
dataIndex: 'cpu',
key: 'cpu',
- render: (val: number) => (
- <Progress
- percent={val}
- size="small"
- status={val > 80 ? 'exception' : 'normal'}
- style={{ width: 100 }}
- />
- ),
- sorter: (a, b) => a.cpu - b.cpu,
+ render: (val: number | null) =>
+ val == null ? (
+ renderUnavailable()
+ ) : (
+ <Progress
+ percent={val}
+ size="small"
+ status={val > 80 ? 'exception' : 'normal'}
+ style={{ width: 100 }}
+ />
+ ),
+ sorter: (a, b) => compareNullable(a.cpu, b.cpu),
},
{
title: t('proxy.uptime'),
dataIndex: 'uptime',
key: 'uptime',
+ render: (value: string | null) => value || renderUnavailable(),
},
{
title: t('proxy.action'),
@@ -331,12 +352,11 @@ const ProxyPage: React.FC = () => {
<Card>
<Statistic
title={t('proxy.healthyNodes')}
- value={clusterStats.healthyNodes}
- suffix={`/ ${clusterStats.totalNodes}`}
- valueStyle={{
- color:
- clusterStats.healthyNodes === clusterStats.totalNodes ?
'#3f8600' : '#cf1322',
- }}
+ value={clusterStats.healthyNodes ?? t('common.na')}
+ suffix={
+ clusterStats.healthyNodes == null ? undefined : `/
${clusterStats.totalNodes}`
+ }
+ valueStyle={{ color: clusterStats.healthyNodes == null ?
undefined : '#3f8600' }}
/>
</Card>
</Col>
@@ -344,7 +364,7 @@ const ProxyPage: React.FC = () => {
<Card>
<Statistic
title={t('proxy.totalConnections')}
- value={clusterStats.totalConnections}
+ value={clusterStats.totalConnections ?? t('common.na')}
valueStyle={{ color: '#1890ff' }}
/>
</Card>
@@ -353,7 +373,7 @@ const ProxyPage: React.FC = () => {
<Card>
<Statistic
title={t('proxy.totalTps')}
- value={clusterStats.totalTPS}
+ value={clusterStats.totalTPS ?? t('common.na')}
valueStyle={{ color: '#1890ff' }}
/>
</Card>
diff --git a/web/src/pages/studio/__tests__/BrokerCluster.test.tsx
b/web/src/pages/studio/__tests__/BrokerCluster.test.tsx
index 03a5ccd4..fde4dc56 100644
--- a/web/src/pages/studio/__tests__/BrokerCluster.test.tsx
+++ b/web/src/pages/studio/__tests__/BrokerCluster.test.tsx
@@ -62,7 +62,7 @@ const clusterFixture: ClusterInfo[] = [
version: '5.3.0',
brokers: [
{
- name: 'broker-a',
+ name: 'broker-api-a',
addr: '10.0.1.10:10911',
version: '5.3.0',
status: 'running',
@@ -71,7 +71,7 @@ const clusterFixture: ClusterInfo[] = [
tpsOut: 8340,
},
{
- name: 'broker-b',
+ name: 'broker-api-b',
addr: '10.0.1.11:10911',
version: '5.3.0',
status: 'readonly',
@@ -89,7 +89,7 @@ const clusterFixture: ClusterInfo[] = [
remotingPort: 8080,
},
],
- nameServers: [{ addr: 'nameserver-a', status: 'healthy' }],
+ nameServers: [{ addr: 'nameserver-api-a', status: 'healthy' }],
config: {
flushDiskType: 'SYNC_FLUSH',
autoCreateTopicEnable: false,
@@ -140,14 +140,15 @@ describe('BrokerCluster Page', () => {
it('should display broker tab with data from the API', async () => {
renderWithProviders(<BrokerCluster />);
// Default tab is broker - data is loaded asynchronously from the service
- const brokerA = await screen.findAllByText('broker-a');
+ const brokerA = await screen.findAllByText('broker-api-a');
expect(brokerA.length).toBeGreaterThan(0);
- expect(screen.getAllByText('broker-b').length).toBeGreaterThan(0);
+ expect(screen.getAllByText('broker-api-b').length).toBeGreaterThan(0);
+ expect(screen.queryByText('broker-a')).not.toBeInTheDocument();
});
it('should display broker status tags', async () => {
renderWithProviders(<BrokerCluster />);
- await screen.findAllByText('broker-a');
+ await screen.findAllByText('broker-api-a');
const runningTags = screen.getAllByText('运行中');
expect(runningTags.length).toBeGreaterThan(0);
const readonlyTags = screen.getAllByText('只读');
@@ -157,17 +158,18 @@ describe('BrokerCluster Page', () => {
it('should switch to NameServer tab on click', async () => {
const user = userEvent.setup();
renderWithProviders(<BrokerCluster />);
- await screen.findByText('broker-a');
+ await screen.findByText('broker-api-a');
const nsTab = screen.getByText('NameServer 管理');
await user.click(nsTab);
// After clicking, NameServer data should be visible (name equals address,
so it appears twice)
- expect(screen.getAllByText('nameserver-a').length).toBeGreaterThan(0);
+ expect(screen.getAllByText('nameserver-api-a').length).toBeGreaterThan(0);
+ expect(screen.queryByText('nameserver-a')).not.toBeInTheDocument();
});
it('should switch to Proxy tab on click', async () => {
const user = userEvent.setup();
renderWithProviders(<BrokerCluster />);
- await screen.findByText('broker-a');
+ await screen.findByText('broker-api-a');
const proxyTab = screen.getByText('Proxy 管理');
await user.click(proxyTab);
// After clicking, Proxy data should be visible (proxy name equals its
address, so it appears twice)
@@ -176,19 +178,22 @@ describe('BrokerCluster Page', () => {
it('should render config and restart action buttons', async () => {
renderWithProviders(<BrokerCluster />);
- await screen.findByText('broker-a');
+ await screen.findByText('broker-api-a');
const configButtons = screen.getAllByText('配置');
expect(configButtons.length).toBeGreaterThan(0);
const restartButtons = screen.getAllByText('重启');
expect(restartButtons.length).toBeGreaterThan(0);
});
- it('should fall back to mock data when the API fails', async () => {
+ it('does not show mock infrastructure data when the API fails', async () => {
vi.mocked(listClusters).mockRejectedValueOnce(new Error('network error'));
renderWithProviders(<BrokerCluster />);
- // Initial state holds the mock fallback rows
await waitFor(() => {
- expect(screen.getByText('broker-a')).toBeInTheDocument();
+ expect(listClusters).toHaveBeenCalledTimes(1);
});
+ expect(screen.queryByText('broker-a')).not.toBeInTheDocument();
+ expect(screen.queryByText('broker-b')).not.toBeInTheDocument();
+ expect(screen.queryByText('nameserver-a')).not.toBeInTheDocument();
+ expect(screen.queryByText('proxy-a')).not.toBeInTheDocument();
});
});
diff --git a/web/src/pages/studio/__tests__/Proxy.test.tsx
b/web/src/pages/studio/__tests__/Proxy.test.tsx
index a79e2720..ae69fe66 100644
--- a/web/src/pages/studio/__tests__/Proxy.test.tsx
+++ b/web/src/pages/studio/__tests__/Proxy.test.tsx
@@ -104,4 +104,12 @@ describe('ProxyPage', () => {
expect(screen.queryByText('proxy.maxConnections')).not.toBeInTheDocument();
expect(screen.queryByText('rocketmq.namesrv.addr')).not.toBeInTheDocument();
});
+
+ it('marks runtime metrics unavailable when proxy API only returns
addresses', async () => {
+ renderPage();
+ await screen.findByText('127.0.0.1:8081');
+
+ expect(screen.queryByText('5.3.0')).not.toBeInTheDocument();
+ expect(screen.getAllByText('N/A').length).toBeGreaterThanOrEqual(5);
+ });
});