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 fc468941 feat(web): switch data source at runtime and consume real 
APIs (#799)
fc468941 is described below

commit fc46894108a7c12bb06d41d634965da9022671f0
Author: lizhimins <[email protected]>
AuthorDate: Mon Aug 3 14:33:07 2026 +0800

    feat(web): switch data source at runtime and consume real APIs (#799)
---
 web/src/App.test.tsx                               |   1 +
 web/src/App.tsx                                    |   6 +-
 web/src/config.ts                                  |   9 +-
 web/src/index.css                                  |   9 +
 web/src/layouts/MainLayout.tsx                     |  43 +++
 web/src/pages/cluster/clients.tsx                  |   3 +-
 web/src/pages/instance/consumer.tsx                |  11 +-
 web/src/pages/instance/index.tsx                   |   5 +-
 web/src/pages/instance/topic.tsx                   |  54 ++-
 web/src/pages/studio/BrokerCluster.tsx             | 380 ++++++++++++++++-----
 web/src/pages/studio/GroupManagement.tsx           | 378 +++++++++++++++++---
 .../pages/studio/__tests__/BrokerCluster.test.tsx  | 122 +++++--
 .../studio/__tests__/GroupManagement.test.tsx      |  66 +++-
 web/src/services/aclService.test.ts                |   2 +-
 web/src/services/aclService.ts                     |  18 +-
 web/src/services/clusterService.test.ts            |   2 +-
 web/src/services/clusterService.ts                 |  34 +-
 web/src/services/connectionsService.test.ts        |   2 +-
 web/src/services/connectionsService.ts             |   4 +-
 web/src/services/consumerService.test.ts           |   2 +-
 web/src/services/consumerService.ts                |  24 +-
 web/src/services/dashboardService.test.ts          |   2 +-
 web/src/services/dashboardService.ts               |   4 +-
 web/src/services/dataMode.ts                       |  27 ++
 web/src/services/instanceService.test.ts           |   2 +-
 web/src/services/instanceService.ts                |  11 +-
 web/src/services/messageService.test.ts            |   2 +-
 web/src/services/messageService.ts                 |  10 +-
 web/src/services/opsService.test.ts                |   2 +-
 web/src/services/opsService.ts                     |  24 +-
 web/src/services/topicService.test.ts              |   2 +-
 web/src/services/topicService.ts                   |  16 +-
 web/src/stores/dataModeStore.ts                    |  36 ++
 33 files changed, 1060 insertions(+), 253 deletions(-)

diff --git a/web/src/App.test.tsx b/web/src/App.test.tsx
index 13c152de..3a5d69ec 100644
--- a/web/src/App.test.tsx
+++ b/web/src/App.test.tsx
@@ -29,6 +29,7 @@ vi.mock('./api/auth', async (importOriginal) => {
 });
 
 vi.mock('./config', () => ({ API_BASE_URL: '/api', USE_MOCK: false }));
+vi.mock('./services/dataMode', () => ({ isMockMode: () => false }));
 
 const mockedGetAuthStatus = vi.mocked(getAuthStatus);
 
diff --git a/web/src/App.tsx b/web/src/App.tsx
index fa54c406..4e98d064 100644
--- a/web/src/App.tsx
+++ b/web/src/App.tsx
@@ -19,7 +19,7 @@ import { lazy, Suspense, useCallback, useEffect, useState } 
from 'react';
 import { Button, Result, Spin } from 'antd';
 import { Routes, Route, Navigate, Outlet } from 'react-router-dom';
 import { getAuthStatus } from './api/auth';
-import { USE_MOCK } from './config';
+import { isMockMode } from './services/dataMode';
 import { useLang } from './i18n/LangContext';
 import useAuthStore from './stores/authStore';
 import MainLayout from './layouts/MainLayout';
@@ -56,11 +56,11 @@ type AuthGateState = 'checking' | 'allowed' | 'denied' | 
'error';
 export function AuthGate() {
   const { t } = useLang();
   const clearAuth = useAuthStore((state) => state.logout);
-  const [gateState, setGateState] = useState<AuthGateState>(USE_MOCK ? 
'allowed' : 'checking');
+  const [gateState, setGateState] = useState<AuthGateState>(isMockMode() ? 
'allowed' : 'checking');
   const [attempt, setAttempt] = useState(0);
 
   useEffect(() => {
-    if (USE_MOCK) return;
+    if (isMockMode()) return;
 
     let cancelled = false;
     void getAuthStatus()
diff --git a/web/src/config.ts b/web/src/config.ts
index 8d58a72c..4068a3f5 100644
--- a/web/src/config.ts
+++ b/web/src/config.ts
@@ -1,10 +1,13 @@
 /*
  * Compile-time configuration.
- * VITE_USE_MOCK controls whether the app uses mock data or real API calls.
- * Set via .env file or build-time environment variable.
  */
 
-export const USE_MOCK = import.meta.env.VITE_USE_MOCK === 'true';
+/**
+ * @deprecated Use `isMockMode()` from `./services/dataMode` instead.
+ * The data mode is now a runtime toggle persisted in localStorage via Zustand.
+ * This export is kept temporarily for backward compatibility and will be 
removed.
+ */
+export const USE_MOCK = false;
 
 /** API prefix for browser requests. Defaults to the reverse-proxy friendly 
`/api`. */
 export const API_BASE_URL = (import.meta.env.VITE_API_BASE_URL || 
'/api').replace(/\/$/, '');
diff --git a/web/src/index.css b/web/src/index.css
index d9074ce1..1c6f07b7 100644
--- a/web/src/index.css
+++ b/web/src/index.css
@@ -284,3 +284,12 @@ body {
   color: #262626;
   font-weight: 600;
 }
+
+/* Tables keep every cell on one line: wrapped cells make rows uneven and hard 
to scan.
+   Long values are truncated with an ellipsis, and the table scrolls 
horizontally instead. */
+.ant-table-cell {
+  white-space: nowrap;
+}
+.ant-table-cell .ant-typography {
+  margin-bottom: 0;
+}
diff --git a/web/src/layouts/MainLayout.tsx b/web/src/layouts/MainLayout.tsx
index 71cbac71..cb8e0f2d 100644
--- a/web/src/layouts/MainLayout.tsx
+++ b/web/src/layouts/MainLayout.tsx
@@ -47,6 +47,7 @@ import {
   isNavigationSearchShortcut,
   type NavigationSearchEntry,
 } from './navigationSearch';
+import { useDataModeStore } from '../stores/dataModeStore';
 
 const { Sider, Content } = Layout;
 
@@ -60,6 +61,14 @@ const MainLayout = () => {
   const [searchText, setSearchText] = useState('');
   const { lang, setLang, t } = useLang();
   const clearAuth = useAuthStore((state) => state.logout);
+  const useMock = useDataModeStore((state) => state.useMock);
+  const toggleDataMode = useDataModeStore((state) => state.toggle);
+
+  // Pages fetch on mount, so reload to re-request everything from the new 
data source.
+  const handleDataModeToggle = () => {
+    toggleDataMode();
+    window.location.reload();
+  };
 
   const handleUserMenuClick = async ({ key }: { key: string }) => {
     if (key === 'profile') {
@@ -302,6 +311,40 @@ const MainLayout = () => {
                 </span>
               </div>
 
+              {/* Data mode toggle */}
+              <div
+                onClick={handleDataModeToggle}
+                style={{
+                  cursor: 'pointer',
+                  display: 'flex',
+                  alignItems: 'center',
+                  gap: 6,
+                  padding: '4px 10px',
+                  borderRadius: 6,
+                  border: `1px solid ${borderColor}`,
+                  fontSize: 12,
+                  fontWeight: 500,
+                  color: useMock ? '#d48806' : '#389e0d',
+                  transition: 'all 0.2s',
+                }}
+                title={
+                  useMock
+                    ? 'Data Mode: Mock (click to switch to Real)'
+                    : 'Data Mode: Real (click to switch to Mock)'
+                }
+              >
+                <span
+                  style={{
+                    width: 8,
+                    height: 8,
+                    borderRadius: '50%',
+                    background: useMock ? '#faad14' : '#52c41a',
+                    display: 'inline-block',
+                  }}
+                />
+                {useMock ? 'Mock' : 'Real'}
+              </div>
+
               {/* Language toggle */}
               <div
                 onClick={() => setLang(lang === 'zh' ? 'en' : 'zh')}
diff --git a/web/src/pages/cluster/clients.tsx 
b/web/src/pages/cluster/clients.tsx
index 351b6634..baa8d789 100644
--- a/web/src/pages/cluster/clients.tsx
+++ b/web/src/pages/cluster/clients.tsx
@@ -39,6 +39,7 @@ import PageHeader from '../../components/PageHeader';
 import { useLang } from '../../i18n/LangContext';
 import type { ClientConnection } from '../../api/connections';
 import { listConnections } from '../../services/connectionsService';
+import { formatDateTime } from '../../utils/format';
 
 const { Text } = Typography;
 
@@ -264,7 +265,7 @@ const ClientsPage = () => {
       sorter: (a, b) => a.connectedAt.localeCompare(b.connectedAt),
       render: (d: string) => (
         <Text type="secondary" style={{ fontSize: 13 }}>
-          {d}
+          {formatDateTime(d)}
         </Text>
       ),
     },
diff --git a/web/src/pages/instance/consumer.tsx 
b/web/src/pages/instance/consumer.tsx
index f21abb00..9b6491c1 100644
--- a/web/src/pages/instance/consumer.tsx
+++ b/web/src/pages/instance/consumer.tsx
@@ -61,6 +61,7 @@ import type { Dayjs } from 'dayjs';
 import PageHeader from '../../components/PageHeader';
 import { useLang } from '../../i18n/LangContext';
 import { TOPIC_TYPE_MAP, PROTOCOL_MAP } from '../../constants/theme';
+import { formatDateTime } from '../../utils/format';
 import type {
   ConsumerGroup,
   ConsumerInstance,
@@ -111,12 +112,6 @@ const formatDelay = (totalSeconds: number): string => {
   return parts.length > 0 ? parts.join('') : '0秒';
 };
 
-const formatDateTime = (dateStr: string): string => {
-  const d = new Date(dateStr);
-  const pad = (n: number) => String(n).padStart(2, '0');
-  return `${d.getFullYear()}-${pad(d.getMonth() + 1)}-${pad(d.getDate())} 
${pad(d.getHours())}:${pad(d.getMinutes())}:${pad(d.getSeconds())}`;
-};
-
 const normalizedConsistency = (value?: string | null): string => 
value?.trim().toLowerCase() ?? '';
 
 const isConsistentValue = (value?: string | null): boolean =>
@@ -326,7 +321,7 @@ const ConsumerPage = () => {
       sorter: (a, b) => a.createdAt.localeCompare(b.createdAt),
       render: (d: string) => (
         <Text type="secondary" style={{ fontSize: 13 }}>
-          {d}
+          {formatDateTime(d)}
         </Text>
       ),
     },
@@ -338,7 +333,7 @@ const ConsumerPage = () => {
       sorter: (a, b) => a.updatedAt.localeCompare(b.updatedAt),
       render: (d: string) => (
         <Text type="secondary" style={{ fontSize: 13 }}>
-          {d}
+          {formatDateTime(d)}
         </Text>
       ),
     },
diff --git a/web/src/pages/instance/index.tsx b/web/src/pages/instance/index.tsx
index 225b806d..5ef21347 100644
--- a/web/src/pages/instance/index.tsx
+++ b/web/src/pages/instance/index.tsx
@@ -35,6 +35,7 @@ import { Plus, MagnifyingGlass } from '@phosphor-icons/react';
 import { EditOutlined, DeleteOutlined } from '@ant-design/icons';
 import type { ColumnsType } from 'antd/es/table';
 import type { Instance, InstanceQuery } from '../../api/instance';
+import { formatDateTime } from '../../utils/format';
 import {
   createInstance,
   deleteInstance,
@@ -213,7 +214,7 @@ const InstancePage = () => {
       sorter: (a, b) => a.createdAt.localeCompare(b.createdAt),
       render: (d: string) => (
         <Text type="secondary" style={{ fontSize: 13 }}>
-          {d}
+          {formatDateTime(d)}
         </Text>
       ),
     },
@@ -225,7 +226,7 @@ const InstancePage = () => {
       sorter: (a, b) => a.updatedAt.localeCompare(b.updatedAt),
       render: (d: string) => (
         <Text type="secondary" style={{ fontSize: 13 }}>
-          {d}
+          {formatDateTime(d)}
         </Text>
       ),
     },
diff --git a/web/src/pages/instance/topic.tsx b/web/src/pages/instance/topic.tsx
index 9c0967a3..b8ae793a 100644
--- a/web/src/pages/instance/topic.tsx
+++ b/web/src/pages/instance/topic.tsx
@@ -17,6 +17,7 @@
 
 import { useEffect, useState, useMemo } from 'react';
 import {
+  Alert,
   Table,
   Card,
   Tag,
@@ -227,6 +228,8 @@ const TopicPage = () => {
   const [tablePageSize, setTablePageSize] = useState(20);
   const [viewMode, setViewMode] = useState<string>('列表');
   const [detailModalOpen, setDetailModalOpen] = useState(false);
+  const [detailLoading, setDetailLoading] = useState(false);
+  const [rebuilding, setRebuilding] = useState(false);
   const [selectedTopic, setSelectedTopic] = useState<Topic | null>(null);
   const [modalOpen, setModalOpen] = useState(false);
   const [form] = Form.useForm();
@@ -279,6 +282,7 @@ const TopicPage = () => {
   const openDetail = async (topic: Topic) => {
     setSelectedTopic(topic);
     setDetailModalOpen(true);
+    setDetailLoading(true);
     try {
       const [routes, consumers] = await Promise.all([
         getTopicRoutes(topic.name),
@@ -288,6 +292,28 @@ const TopicPage = () => {
       setConsumersByTopic((previous) => ({ ...previous, [topic.name]: 
consumers }));
     } catch {
       message.error('Topic 详情加载失败,请稍后重试');
+    } finally {
+      setDetailLoading(false);
+    }
+  };
+
+  // Metadata lives in the database, so a record can exist without a broker 
route.
+  const rebuildTopic = async (topic: Topic) => {
+    setRebuilding(true);
+    try {
+      await createTopic({
+        name: topic.name,
+        type: topic.type,
+        writeQueues: topic.writeQueues,
+        readQueues: topic.readQueues,
+      });
+      const routes = await getTopicRoutes(topic.name);
+      setRoutesByTopic((previous) => ({ ...previous, [topic.name]: routes }));
+      message.success(`Topic「${topic.name}」已在 Broker 上重建`);
+    } catch {
+      message.error('重建 Topic 失败,请检查 Broker 状态后重试');
+    } finally {
+      setRebuilding(false);
     }
   };
 
@@ -333,7 +359,7 @@ const TopicPage = () => {
       width: 220,
       sorter: (a, b) => a.name.localeCompare(b.name),
       render: (name: string) => (
-        <Text strong style={{ fontSize: 14 }}>
+        <Text strong style={{ fontSize: 14, display: 'block' }} ellipsis={{ 
tooltip: name }}>
           {name}
         </Text>
       ),
@@ -345,7 +371,11 @@ const TopicPage = () => {
       width: 200,
       sorter: (a, b) => a.remark.localeCompare(b.remark),
       render: (remark: string) => (
-        <Text type="secondary" style={{ fontSize: 13 }}>
+        <Text
+          type="secondary"
+          style={{ fontSize: 13, display: 'block' }}
+          ellipsis={{ tooltip: remark }}
+        >
           {remark}
         </Text>
       ),
@@ -784,12 +814,32 @@ const TopicPage = () => {
             <Text strong style={{ fontSize: 14, display: 'block', 
marginBottom: 12 }}>
               路由信息
             </Text>
+            {!detailLoading && getRoutes(selectedTopic.name).length === 0 && (
+              <Alert
+                type="warning"
+                showIcon
+                style={{ marginBottom: 12 }}
+                message="Broker 上没有该 Topic 的路由"
+                description="元数据库中存在这条记录,但 Broker 未返回路由信息,可能尚未在 Broker 
上创建或已被删除。可按库中记录的队列数重建。"
+                action={
+                  <Button
+                    size="small"
+                    type="primary"
+                    loading={rebuilding}
+                    onClick={() => void rebuildTopic(selectedTopic)}
+                  >
+                    在 Broker 上重建
+                  </Button>
+                }
+              />
+            )}
             <Table<BrokerRoute>
               columns={routeColumns}
               dataSource={getRoutes(selectedTopic.name)}
               rowKey="brokerName"
               pagination={false}
               size="small"
+              loading={detailLoading}
             />
 
             <Divider style={{ margin: '20px 0 16px' }} />
diff --git a/web/src/pages/studio/BrokerCluster.tsx 
b/web/src/pages/studio/BrokerCluster.tsx
index f584d3e5..9e589c0d 100644
--- a/web/src/pages/studio/BrokerCluster.tsx
+++ b/web/src/pages/studio/BrokerCluster.tsx
@@ -15,8 +15,8 @@
  * limitations under the License.
  */
 
-import { useState } from 'react';
-import { Table, Button, Tag, Tabs, Card, Space, Switch, Progress, Tooltip } 
from 'antd';
+import { useCallback, useRef, useState } from 'react';
+import { Table, Button, Tag, Tabs, Card, Space, Switch, Progress, Tooltip, 
Spin, App } from 'antd';
 import {
   Plus,
   ArrowClockwise,
@@ -27,25 +27,29 @@ import {
   PlugsConnected,
 } from '@phosphor-icons/react';
 import { useLang } from '../../i18n/LangContext';
+import { listClusters } from '../../services/clusterService';
+import type { ClusterInfo } from '../../api/cluster';
 
 // ─── Types ──────────────────────────────────────────────────────
+type NodeStatus = 'running' | 'readonly' | 'maintenance';
+
 interface BrokerRecord {
   key: string;
   k8sCluster: string;
   brokerName: string;
-  status: 'running' | 'readonly' | 'maintenance';
+  status: NodeStatus;
   version: string;
   diskUsage: number;
   address: string;
-  tpsIn: string;
-  tpsOut: string;
+  tpsIn: number;
+  tpsOut: number;
 }
 
 interface NameServerRecord {
   key: string;
   k8sCluster: string;
   name: string;
-  status: 'running' | 'readonly' | 'maintenance';
+  status: NodeStatus;
   version: string;
   address: string;
   connections: number;
@@ -55,22 +59,233 @@ interface ProxyRecord {
   key: string;
   k8sCluster: string;
   name: string;
-  status: 'running' | 'readonly' | 'maintenance';
+  status: NodeStatus;
   version: string;
   address: string;
   grpcPort: string;
   connections: number;
 }
 
-const brokerData: BrokerRecord[] = [];
-const nameServerData: NameServerRecord[] = [];
-const proxyData: ProxyRecord[] = [];
+// ─── 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();
+  if (value === 'readonly' || value === 'warning') return 'readonly';
+  if (value === 'maintenance' || value === 'error' || value === 'offline') 
return 'maintenance';
+  return 'running';
+};
+
+const hostOf = (addr: string): string => addr.split(':')[0] ?? addr;
+
+function mapClusters(clusters: ClusterInfo[]): {
+  brokers: BrokerRecord[];
+  nameServers: NameServerRecord[];
+  proxies: ProxyRecord[];
+} {
+  const brokers: BrokerRecord[] = [];
+  const nameServers: NameServerRecord[] = [];
+  const proxies: ProxyRecord[] = [];
+
+  clusters.forEach((cluster) => {
+    const clusterLabel = cluster.nsClusterName || cluster.name || cluster.id;
+
+    cluster.brokers.forEach((broker, index) => {
+      brokers.push({
+        key: `${cluster.id}-broker-${broker.addr || index}`,
+        k8sCluster: clusterLabel,
+        brokerName: broker.name || broker.addr,
+        status: normalizeStatus(broker.status),
+        version: broker.version || cluster.version,
+        diskUsage: broker.diskUsage ?? 0,
+        address: broker.addr,
+        tpsIn: broker.tpsIn ?? 0,
+        tpsOut: broker.tpsOut ?? 0,
+      });
+    });
+
+    cluster.nameServers.forEach((nameServer, index) => {
+      nameServers.push({
+        key: `${cluster.id}-ns-${nameServer.addr || index}`,
+        k8sCluster: clusterLabel,
+        name: nameServer.addr,
+        status: normalizeStatus(nameServer.status),
+        version: cluster.version,
+        address: nameServer.addr,
+        connections: 0,
+      });
+    });
+
+    cluster.proxies.forEach((proxy, index) => {
+      const host = hostOf(proxy.addr);
+      proxies.push({
+        key: `${cluster.id}-proxy-${proxy.addr || index}`,
+        k8sCluster: clusterLabel,
+        name: proxy.addr,
+        status: normalizeStatus(proxy.status),
+        version: cluster.version,
+        address: proxy.addr,
+        grpcPort: proxy.grpcPort ? `${host}:${proxy.grpcPort}` : '-',
+        connections: proxy.connections ?? 0,
+      });
+    });
+  });
+
+  return { brokers, nameServers, proxies };
+}
 
 // ─── Component ──────────────────────────────────────────────────
 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 { t } = useLang();
+  const { message } = App.useApp();
+
+  const loadData = useCallback(async () => {
+    setLoading(true);
+    try {
+      const clusters = await listClusters();
+      const mapped = mapClusters(clusters);
+      setBrokerData(mapped.brokers);
+      setNameServerData(mapped.nameServers);
+      setProxyData(mapped.proxies);
+    } catch {
+      message.error(t('common.refreshFailed'));
+    } finally {
+      setLoading(false);
+    }
+  }, [message, t]);
+
+  const initialized = useRef<boolean | null>(null);
+  if (initialized.current == null) {
+    initialized.current = true;
+    void loadData();
+  }
 
   const renderStatus = (status: string) => {
     const config: Record<string, { color: string; label: string }> = {
@@ -157,17 +372,15 @@ const BrokerClusterPage = () => {
       title: t('brokerCluster.tpsIn'),
       dataIndex: 'tpsIn',
       key: 'tpsIn',
-      render: (text: string) => <span style={{ fontWeight: 500 
}}>{text}</span>,
-      sorter: (a: BrokerRecord, b: BrokerRecord) =>
-        parseFloat(a.tpsIn.replace(/,/g, '')) - 
parseFloat(b.tpsIn.replace(/,/g, '')),
+      render: (value: number) => <span style={{ fontWeight: 500 
}}>{value.toLocaleString()}</span>,
+      sorter: (a: BrokerRecord, b: BrokerRecord) => a.tpsIn - b.tpsIn,
     },
     {
       title: t('brokerCluster.tpsOut'),
       dataIndex: 'tpsOut',
       key: 'tpsOut',
-      render: (text: string) => <span style={{ fontWeight: 500 
}}>{text}</span>,
-      sorter: (a: BrokerRecord, b: BrokerRecord) =>
-        parseFloat(a.tpsOut.replace(/,/g, '')) - 
parseFloat(b.tpsOut.replace(/,/g, '')),
+      render: (value: number) => <span style={{ fontWeight: 500 
}}>{value.toLocaleString()}</span>,
+      sorter: (a: BrokerRecord, b: BrokerRecord) => a.tpsOut - b.tpsOut,
     },
     {
       title: t('common.actions'),
@@ -230,7 +443,7 @@ const BrokerClusterPage = () => {
       title: t('brokerCluster.connections'),
       dataIndex: 'connections',
       key: 'connections',
-      render: (text: number) => <span style={{ fontWeight: 500 
}}>{text}</span>,
+      render: (text: number) => <span style={{ fontWeight: 500 
}}>{text.toLocaleString()}</span>,
     },
     {
       title: t('common.actions'),
@@ -306,7 +519,7 @@ const BrokerClusterPage = () => {
       title: t('brokerCluster.connections'),
       dataIndex: 'connections',
       key: 'connections',
-      render: (text: number) => <span style={{ fontWeight: 500 
}}>{text}</span>,
+      render: (text: number) => <span style={{ fontWeight: 500 
}}>{text.toLocaleString()}</span>,
     },
     {
       title: t('common.actions'),
@@ -354,7 +567,7 @@ const BrokerClusterPage = () => {
             unCheckedChildren={t('brokerCluster.manual')}
             size="small"
           />
-          <Button icon={<ArrowClockwise size={14} />} size="small">
+          <Button icon={<ArrowClockwise size={14} />} size="small" onClick={() 
=> void loadData()}>
             {t('common.reset')}
           </Button>
           <Button type="primary" icon={<Plus size={14} />}>
@@ -363,71 +576,70 @@ const BrokerClusterPage = () => {
         </Space>
       </div>
 
-      <Card bordered={false} style={{ borderRadius: 8, boxShadow: '0 1px 6px 
rgba(0,0,0,0.04)' }}>
-        <Tabs
-          activeKey={activeTab}
-          onChange={setActiveTab}
-          items={[
-            {
-              key: 'nameserver',
-              label: (
-                <span>
-                  <ChartBar size={16} style={{ marginRight: 4, verticalAlign: 
'middle' }} />
-                  {t('brokerCluster.nsManagement')}
-                </span>
-              ),
-              children: (
-                <Table
-                  columns={nsColumns}
-                  dataSource={nameServerData}
-                  locale={{ emptyText: t('brokerCluster.providerUnavailable') 
}}
-                  pagination={false}
-                  size="middle"
-                />
-              ),
-            },
-            {
-              key: 'broker',
-              label: (
-                <span>
-                  <Cloud size={16} style={{ marginRight: 4, verticalAlign: 
'middle' }} />
-                  {t('brokerCluster.brokerManagement')}
-                </span>
-              ),
-              children: (
-                <Table
-                  columns={brokerColumns}
-                  dataSource={brokerData}
-                  locale={{ emptyText: t('brokerCluster.providerUnavailable') 
}}
-                  pagination={{
-                    pageSize: 10,
-                    showTotal: (total) => `${t('common.total')} ${total} 
Broker`,
-                  }}
-                  size="middle"
-                />
-              ),
-            },
-            {
-              key: 'proxy',
-              label: (
-                <span>
-                  <PlugsConnected size={16} style={{ marginRight: 4, 
verticalAlign: 'middle' }} />
-                  {t('brokerCluster.proxyManagement')}
-                </span>
-              ),
-              children: (
-                <Table
-                  columns={proxyColumns}
-                  dataSource={proxyData}
-                  locale={{ emptyText: t('brokerCluster.providerUnavailable') 
}}
-                  pagination={false}
-                  size="middle"
-                />
-              ),
-            },
-          ]}
-        />
-      </Card>
+      <Spin spinning={loading} tip={t('common.loading')}>
+        <Card bordered={false} style={{ borderRadius: 8, boxShadow: '0 1px 6px 
rgba(0,0,0,0.04)' }}>
+          <Tabs
+            activeKey={activeTab}
+            onChange={setActiveTab}
+            items={[
+              {
+                key: 'nameserver',
+                label: (
+                  <span>
+                    <ChartBar size={16} style={{ marginRight: 4, 
verticalAlign: 'middle' }} />
+                    {t('brokerCluster.nsManagement')}
+                  </span>
+                ),
+                children: (
+                  <Table
+                    columns={nsColumns}
+                    dataSource={nameServerData}
+                    pagination={false}
+                    size="middle"
+                  />
+                ),
+              },
+              {
+                key: 'broker',
+                label: (
+                  <span>
+                    <Cloud size={16} style={{ marginRight: 4, verticalAlign: 
'middle' }} />
+                    {t('brokerCluster.brokerManagement')}
+                  </span>
+                ),
+                children: (
+                  <Table
+                    columns={brokerColumns}
+                    dataSource={brokerData}
+                    pagination={{
+                      pageSize: 10,
+                      showTotal: (total) => `${t('common.total')} ${total} 
Broker`,
+                    }}
+                    size="middle"
+                  />
+                ),
+              },
+              {
+                key: 'proxy',
+                label: (
+                  <span>
+                    <PlugsConnected size={16} style={{ marginRight: 4, 
verticalAlign: 'middle' }} />
+                    {t('brokerCluster.proxyManagement')}
+                  </span>
+                ),
+                children: (
+                  <Table
+                    columns={proxyColumns}
+                    dataSource={proxyData}
+                    pagination={false}
+                    size="middle"
+                  />
+                ),
+              },
+            ]}
+          />
+        </Card>
+      </Spin>
     </div>
   );
 };
diff --git a/web/src/pages/studio/GroupManagement.tsx 
b/web/src/pages/studio/GroupManagement.tsx
index 0a648b04..7727b231 100644
--- a/web/src/pages/studio/GroupManagement.tsx
+++ b/web/src/pages/studio/GroupManagement.tsx
@@ -15,68 +15,149 @@
  * limitations under the License.
  */
 
-import { useState } from 'react';
+import { useCallback, useEffect, useMemo, useState } from 'react';
 import {
   Table,
   Button,
   Input,
   Tag,
+  Modal,
+  Tabs,
   Card,
+  Row,
+  Col,
+  Descriptions,
   Space,
   Switch,
+  message,
 } from 'antd';
-import { MagnifyingGlass, Plus, ArrowClockwise, Users } from 
'@phosphor-icons/react';
+import { MagnifyingGlass, Plus, ArrowClockwise, Users, Eye } from 
'@phosphor-icons/react';
 import { useLang } from '../../i18n/LangContext';
+import type { ConsumerGroup, QueueProgress, SubscriptionEntry } from 
'../../api/metadata';
+import {
+  getConsumerProgress,
+  getConsumerSubscriptions,
+  listConsumerGroups,
+} from '../../services/consumerService';
+
+// ─── Helpers ────────────────────────────────────────────────────
+type GroupStatus = 'running' | 'warning' | 'stopped';
+
+const BACKLOG_WARNING_THRESHOLD = 10000;
+
+const deriveStatus = (group: ConsumerGroup): GroupStatus => {
+  if (group.onlineInstances <= 0) return 'stopped';
+  if (group.totalLag > BACKLOG_WARNING_THRESHOLD) return 'warning';
+  return 'running';
+};
 
-// ─── Types ──────────────────────────────────────────────────────
-interface GroupRecord {
-  key: string;
-  group: string;
-  namespace: string;
-  cluster: string;
-  count: number;
-  consumeType: string;
-  messageModel: 'CLUSTERING' | 'BROADCASTING';
-  diff: number;
-  status: 'running' | 'warning' | 'stopped';
-}
-
-const groupData: GroupRecord[] = [];
+const isConsistent = (consistency: string): boolean =>
+  consistency === 'consistent' || consistency === '一致';
 
 // ─── Component ──────────────────────────────────────────────────
 const GroupManagementPage = () => {
   const [searchText, setSearchText] = useState('');
+  const [modalVisible, setModalVisible] = useState(false);
+  const [selectedGroup, setSelectedGroup] = useState<ConsumerGroup | 
null>(null);
   const [autoRefresh, setAutoRefresh] = useState(false);
+  const [groups, setGroups] = useState<ConsumerGroup[]>([]);
+  const [loading, setLoading] = useState(true);
+  const [subscriptions, setSubscriptions] = useState<SubscriptionEntry[]>([]);
+  const [progress, setProgress] = useState<QueueProgress[]>([]);
+  const [detailLoading, setDetailLoading] = useState(false);
   const { t } = useLang();
 
+  useEffect(() => {
+    let cancelled = false;
+
+    const fetchGroups = async () => {
+      try {
+        const data = await listConsumerGroups();
+        if (!cancelled) setGroups(data);
+      } catch {
+        if (!cancelled) message.error(t('consumer.fetchListFailed'));
+      } finally {
+        if (!cancelled) setLoading(false);
+      }
+    };
+
+    void fetchGroups();
+    return () => {
+      cancelled = true;
+    };
+  }, [t]);
+
+  const handleRefresh = useCallback(async () => {
+    setLoading(true);
+    try {
+      const data = await listConsumerGroups();
+      setGroups(data);
+    } catch {
+      message.error(t('consumer.fetchListFailed'));
+    } finally {
+      setLoading(false);
+    }
+  }, [t]);
+
+  const handleViewDetail = useCallback(
+    async (group: ConsumerGroup) => {
+      setSelectedGroup(group);
+      setModalVisible(true);
+      setSubscriptions([]);
+      setProgress([]);
+      setDetailLoading(true);
+      try {
+        const [subs, prog] = await Promise.all([
+          getConsumerSubscriptions(group.name),
+          getConsumerProgress(group.name),
+        ]);
+        setSubscriptions(subs);
+        setProgress(prog);
+      } catch {
+        message.error(t('consumer.fetchProgressFailed', { name: group.name }));
+      } finally {
+        setDetailLoading(false);
+      }
+    },
+    [t],
+  );
+
   const normalizedSearchText = searchText.trim().toLowerCase();
-  const filteredGroupData = groupData.filter(
-    (record) => !normalizedSearchText || 
record.group.toLowerCase().includes(normalizedSearchText),
+  const filteredGroupData = useMemo(
+    () =>
+      groups.filter(
+        (record) =>
+          !normalizedSearchText || 
record.name.toLowerCase().includes(normalizedSearchText),
+      ),
+    [groups, normalizedSearchText],
   );
 
   const columns = [
     {
       title: t('groupMgmt.groupName'),
-      dataIndex: 'group',
-      key: 'group',
-      render: (text: string) => (
-        <span style={{ color: '#1677ff', fontWeight: 500 }}>
+      dataIndex: 'name',
+      key: 'name',
+      render: (text: string, record: ConsumerGroup) => (
+        <a
+          onClick={() => void handleViewDetail(record)}
+          style={{ color: '#1677ff', fontWeight: 500, whiteSpace: 'nowrap' }}
+        >
           {text}
-        </span>
+        </a>
       ),
     },
     { title: t('groupMgmt.namespace'), dataIndex: 'namespace', key: 
'namespace' },
-    { title: t('groupMgmt.cluster'), dataIndex: 'cluster', key: 'cluster' },
+    { title: t('groupMgmt.cluster'), dataIndex: 'clusterId', key: 'clusterId' 
},
     {
       title: t('groupMgmt.onlineInstances'),
-      dataIndex: 'count',
-      key: 'count',
+      dataIndex: 'onlineInstances',
+      key: 'onlineInstances',
       render: (count: number) => <span style={{ fontWeight: 500 
}}>{count}</span>,
     },
     {
       title: t('groupMgmt.consumeMode'),
-      dataIndex: 'messageModel',
-      key: 'messageModel',
+      dataIndex: 'consumeType',
+      key: 'consumeType',
       render: (mode: string) => (
         <Tag color={mode === 'CLUSTERING' ? 'blue' : 'orange'}>
           {mode === 'CLUSTERING' ? t('groupMgmt.clustering') : 
t('groupMgmt.broadcasting')}
@@ -85,8 +166,8 @@ const GroupManagementPage = () => {
     },
     {
       title: t('groupMgmt.diff'),
-      dataIndex: 'diff',
-      key: 'diff',
+      dataIndex: 'totalLag',
+      key: 'totalLag',
       render: (diff: number) => (
         <span
           style={{
@@ -100,27 +181,27 @@ const GroupManagementPage = () => {
     },
     {
       title: t('brokerCluster.status'),
-      dataIndex: 'status',
       key: 'status',
-      render: (status: string) => {
-        const config: Record<string, { color: string; label: string }> = {
+      render: (_: unknown, record: ConsumerGroup) => {
+        const status = deriveStatus(record);
+        const config: Record<GroupStatus, { color: string; label: string }> = {
           running: { color: 'success', label: t('brokerCluster.statusRunning') 
},
           warning: { color: 'warning', label: t('groupMgmt.backlogAlert') },
           stopped: { color: 'error', label: t('groupMgmt.stopped') },
         };
-        const { color, label } = config[status] || config.running;
+        const { color, label } = config[status];
         return <Tag color={color}>{label}</Tag>;
       },
     },
     {
       title: t('common.actions'),
       key: 'action',
-      render: () => (
+      render: (_: unknown, record: ConsumerGroup) => (
         <Space size="small">
-          <Button type="link" size="small" disabled>
+          <Button type="link" size="small" onClick={() => void 
handleViewDetail(record)}>
             {t('common.detail')}
           </Button>
-          <Button type="link" size="small" disabled>
+          <Button type="link" size="small">
             {t('brokerCluster.config')}
           </Button>
         </Space>
@@ -128,6 +209,45 @@ const GroupManagementPage = () => {
     },
   ];
 
+  const subscriptionColumns = [
+    {
+      title: t('groupMgmt.topic'),
+      dataIndex: 'topic',
+      key: 'topic',
+      render: (text: string) => <span style={{ fontWeight: 500 
}}>{text}</span>,
+    },
+    {
+      title: t('groupMgmt.consistency'),
+      dataIndex: 'consistency',
+      key: 'consistency',
+      render: (consistency: string) => (
+        <Tag color={isConsistent(consistency) ? 'success' : 'warning'}>
+          {isConsistent(consistency) ? t('groupMgmt.consistent') : 
t('groupMgmt.inconsistent')}
+        </Tag>
+      ),
+    },
+    { title: t('groupMgmt.subMode'), dataIndex: 'filterMode', key: 
'filterMode' },
+    {
+      title: t('groupMgmt.expression'),
+      dataIndex: 'expression',
+      key: 'expression',
+      render: (text: string) => (
+        <code style={{ background: '#f5f5f5', padding: '2px 6px', 
borderRadius: 4, fontSize: 12 }}>
+          {text}
+        </code>
+      ),
+    },
+    {
+      title: t('common.actions'),
+      key: 'action',
+      render: () => (
+        <Button type="link" size="small" icon={<Eye size={14} />}>
+          {t('groupMgmt.viewDistribution')}
+        </Button>
+      ),
+    },
+  ];
+
   return (
     <div style={{ padding: 0 }}>
       <div
@@ -169,7 +289,11 @@ const GroupManagementPage = () => {
             unCheckedChildren={t('groupMgmt.manual')}
             size="small"
           />
-          <Button icon={<ArrowClockwise size={14} />} size="small">
+          <Button
+            icon={<ArrowClockwise size={14} />}
+            size="small"
+            onClick={() => void handleRefresh()}
+          >
             {t('common.reset')}
           </Button>
         </Space>
@@ -179,7 +303,8 @@ const GroupManagementPage = () => {
         <Table
           columns={columns}
           dataSource={filteredGroupData}
-          locale={{ emptyText: t('groupMgmt.providerUnavailable') }}
+          rowKey="name"
+          loading={loading}
           pagination={{
             pageSize: 10,
             showTotal: (total) => `${t('common.total')} ${total} Group`,
@@ -188,6 +313,181 @@ const GroupManagementPage = () => {
           size="middle"
         />
       </Card>
+
+      <Modal
+        title={null}
+        open={modalVisible}
+        onCancel={() => setModalVisible(false)}
+        footer={null}
+        width={720}
+        destroyOnClose
+      >
+        <div style={{ marginBottom: 16 }}>
+          <h3 style={{ margin: 0, display: 'flex', alignItems: 'center' }}>
+            <Users size={18} style={{ marginRight: 8, color: '#1677ff' }} />
+            {selectedGroup?.name}
+          </h3>
+        </div>
+        {selectedGroup && (
+          <Tabs
+            defaultActiveKey="overview"
+            items={[
+              {
+                key: 'overview',
+                label: t('groupMgmt.overview'),
+                children: (
+                  <div>
+                    <Row gutter={16} style={{ marginBottom: 20 }}>
+                      <Col span={8}>
+                        <Card bordered={false} style={{ background: '#f6ffed' 
}}>
+                          <div style={{ color: '#666', fontSize: 12 }}>
+                            {t('groupMgmt.onlineInstances')}
+                          </div>
+                          <div style={{ fontSize: 24, fontWeight: 600 }}>
+                            {selectedGroup.onlineInstances}{' '}
+                            <Tag color="success" style={{ marginLeft: 8 }}>
+                              {t('groupMgmt.online')}
+                            </Tag>
+                          </div>
+                        </Card>
+                      </Col>
+                      <Col span={8}>
+                        <Card bordered={false} style={{ background: '#fff2f0' 
}}>
+                          <div style={{ color: '#666', fontSize: 12 }}>
+                            {t('groupMgmt.totalDiff')}
+                          </div>
+                          <div style={{ fontSize: 24, fontWeight: 600, color: 
'#ff4d4f' }}>
+                            {selectedGroup.totalLag.toLocaleString()}
+                          </div>
+                        </Card>
+                      </Col>
+                      <Col span={8}>
+                        <Card bordered={false} style={{ background: '#f0f5ff' 
}}>
+                          <div style={{ color: '#666', fontSize: 12 }}>
+                            {t('groupMgmt.subscribedTopics')}
+                          </div>
+                          <div style={{ fontSize: 24, fontWeight: 600 }}>
+                            {selectedGroup.subscribedTopics.length}
+                          </div>
+                        </Card>
+                      </Col>
+                    </Row>
+                    <Descriptions column={2} bordered size="small">
+                      <Descriptions.Item label={t('groupMgmt.groupName')}>
+                        {selectedGroup.name}
+                      </Descriptions.Item>
+                      <Descriptions.Item label={t('groupMgmt.namespace')}>
+                        {selectedGroup.namespace}
+                      </Descriptions.Item>
+                      <Descriptions.Item label={t('groupMgmt.cluster')}>
+                        {selectedGroup.clusterId}
+                      </Descriptions.Item>
+                      <Descriptions.Item label={t('groupMgmt.consumeMode')}>
+                        <Tag color={selectedGroup.consumeType === 'CLUSTERING' 
? 'blue' : 'orange'}>
+                          {selectedGroup.consumeType === 'CLUSTERING'
+                            ? t('groupMgmt.clustering')
+                            : t('groupMgmt.broadcasting')}
+                        </Tag>
+                      </Descriptions.Item>
+                      <Descriptions.Item label={t('groupMgmt.consumeType')}>
+                        {selectedGroup.subscriptionMode}
+                      </Descriptions.Item>
+                      <Descriptions.Item label={t('groupMgmt.consumeDelay')}>
+                        {selectedGroup.delaySeconds.toLocaleString()}s
+                      </Descriptions.Item>
+                      <Descriptions.Item label={t('groupMgmt.maxRetry')}>
+                        {selectedGroup.retryMaxTimes}
+                      </Descriptions.Item>
+                      <Descriptions.Item label={t('groupMgmt.createdAt')}>
+                        {selectedGroup.createdAt}
+                      </Descriptions.Item>
+                      <Descriptions.Item 
label={t('groupMgmt.subscribedTopics')} span={2}>
+                        {selectedGroup.subscribedTopics.join(', ')}
+                      </Descriptions.Item>
+                    </Descriptions>
+                    <h4 style={{ marginTop: 20, marginBottom: 12 }}>
+                      {t('groupMgmt.subscription')}
+                    </h4>
+                    <Table
+                      columns={subscriptionColumns}
+                      dataSource={subscriptions}
+                      rowKey="topic"
+                      loading={detailLoading}
+                      pagination={false}
+                      size="small"
+                    />
+                  </div>
+                ),
+              },
+              {
+                key: 'instances',
+                label: t('groupMgmt.onlineInstances'),
+                children: (
+                  <Table
+                    columns={[
+                      {
+                        title: t('groupMgmt.instanceId'),
+                        dataIndex: 'clientId',
+                        key: 'clientId',
+                      },
+                      { title: t('common.address'), dataIndex: 'address', key: 
'address' },
+                      { title: t('brokerCluster.version'), dataIndex: 
'protocol', key: 'protocol' },
+                      {
+                        title: t('brokerCluster.status'),
+                        key: 'status',
+                        render: () => <Tag 
color="success">{t('groupMgmt.online')}</Tag>,
+                      },
+                    ]}
+                    dataSource={selectedGroup.instances}
+                    rowKey="clientId"
+                    pagination={false}
+                    size="small"
+                  />
+                ),
+              },
+              {
+                key: 'progress',
+                label: t('groupMgmt.consumeProgress'),
+                children: (
+                  <Table
+                    columns={[
+                      { title: 'Broker', dataIndex: 'broker', key: 'broker' },
+                      { title: 'QueueId', dataIndex: 'queueId', key: 'queueId' 
},
+                      {
+                        title: 'Broker Offset',
+                        dataIndex: 'brokerOffset',
+                        key: 'brokerOffset',
+                        render: (v: number) => v.toLocaleString(),
+                      },
+                      {
+                        title: 'Consumer Offset',
+                        dataIndex: 'consumerOffset',
+                        key: 'consumerOffset',
+                        render: (v: number) => v.toLocaleString(),
+                      },
+                      {
+                        title: 'Diff',
+                        dataIndex: 'diffTotal',
+                        key: 'diffTotal',
+                        render: (v: number) => (
+                          <span style={{ color: v > 100 ? '#ff4d4f' : 
'#52c41a', fontWeight: 500 }}>
+                            {v.toLocaleString()}
+                          </span>
+                        ),
+                      },
+                    ]}
+                    dataSource={progress}
+                    rowKey={(record) => `${record.broker}-${record.queueId}`}
+                    loading={detailLoading}
+                    pagination={false}
+                    size="small"
+                  />
+                ),
+              },
+            ]}
+          />
+        )}
+      </Modal>
     </div>
   );
 };
diff --git a/web/src/pages/studio/__tests__/BrokerCluster.test.tsx 
b/web/src/pages/studio/__tests__/BrokerCluster.test.tsx
index 9d580df3..03a5ccd4 100644
--- a/web/src/pages/studio/__tests__/BrokerCluster.test.tsx
+++ b/web/src/pages/studio/__tests__/BrokerCluster.test.tsx
@@ -15,13 +15,19 @@
  * limitations under the License.
  */
 
-import { describe, it, expect, vi, beforeAll } from 'vitest';
-import { render, screen } from '@testing-library/react';
+import { describe, it, expect, vi, beforeAll, beforeEach } from 'vitest';
+import { render, screen, waitFor } from '@testing-library/react';
 import userEvent from '@testing-library/user-event';
 import { App } from 'antd';
 import { LangProvider } from '../../../i18n/LangContext';
+import { listClusters } from '../../../services/clusterService';
+import type { ClusterInfo } from '../../../api/cluster';
 import BrokerCluster from '../BrokerCluster';
 
+vi.mock('../../../services/clusterService', () => ({
+  listClusters: vi.fn(),
+}));
+
 // Mock matchMedia for antd responsive components
 beforeAll(() => {
   Object.defineProperty(window, 'matchMedia', {
@@ -45,6 +51,63 @@ vi.mock('react-router-dom', () => ({
   useParams: () => ({}),
 }));
 
+const clusterFixture: ClusterInfo[] = [
+  {
+    id: 'cluster-1',
+    name: 'prod-cn-east-1',
+    nsClusterName: 'prod-cn-east-1',
+    type: 'V5_PROXY_CLUSTER',
+    endpoint: '10.0.1.20:9876',
+    status: 'healthy',
+    version: '5.3.0',
+    brokers: [
+      {
+        name: 'broker-a',
+        addr: '10.0.1.10:10911',
+        version: '5.3.0',
+        status: 'running',
+        diskUsage: 62,
+        tpsIn: 12580,
+        tpsOut: 8340,
+      },
+      {
+        name: 'broker-b',
+        addr: '10.0.1.11:10911',
+        version: '5.3.0',
+        status: 'readonly',
+        diskUsage: 89,
+        tpsIn: 0,
+        tpsOut: 3120,
+      },
+    ],
+    proxies: [
+      {
+        addr: '10.0.1.30:8080',
+        status: 'healthy',
+        connections: 2340,
+        grpcPort: 8081,
+        remotingPort: 8080,
+      },
+    ],
+    nameServers: [{ addr: 'nameserver-a', status: 'healthy' }],
+    config: {
+      flushDiskType: 'SYNC_FLUSH',
+      autoCreateTopicEnable: false,
+      autoCreateSubscriptionGroup: false,
+      maxMessageSize: 4194304,
+      msgTraceTopicName: 'RMQ_SYS_TRACE_TOPIC4',
+      fileReservedTime: 72,
+      writeQueueNums: 16,
+      readQueueNums: 16,
+      brokerPermission: 6,
+      deleteWhen: '04',
+    },
+    topicCount: 10,
+    groupCount: 5,
+    tpsHistory: [],
+  },
+];
+
 const renderWithProviders = (ui: React.ReactElement) => {
   return render(
     <App>
@@ -54,6 +117,11 @@ const renderWithProviders = (ui: React.ReactElement) => {
 };
 
 describe('BrokerCluster Page', () => {
+  beforeEach(() => {
+    vi.clearAllMocks();
+    vi.mocked(listClusters).mockResolvedValue(clusterFixture);
+  });
+
   it('should render the page title', () => {
     renderWithProviders(<BrokerCluster />);
     expect(screen.getByText('Broker 集群')).toBeInTheDocument();
@@ -69,44 +137,58 @@ describe('BrokerCluster Page', () => {
     expect(screen.getByText('重置')).toBeInTheDocument();
   });
 
-  it('should show an explicit unavailable state instead of mock broker data', 
() => {
+  it('should display broker tab with data from the API', async () => {
     renderWithProviders(<BrokerCluster />);
-    expect(screen.getByText('当前版本尚未接入真实集群拓扑接口,已停止展示模拟 Broker / NameServer / 
Proxy 数据。')).toBeInTheDocument();
-    expect(screen.queryByText('broker-a')).not.toBeInTheDocument();
-    expect(screen.queryByText('broker-b')).not.toBeInTheDocument();
+    // Default tab is broker - data is loaded asynchronously from the service
+    const brokerA = await screen.findAllByText('broker-a');
+    expect(brokerA.length).toBeGreaterThan(0);
+    expect(screen.getAllByText('broker-b').length).toBeGreaterThan(0);
   });
 
-  it('should not render row status tags without real broker data', () => {
+  it('should display broker status tags', async () => {
     renderWithProviders(<BrokerCluster />);
-    expect(screen.queryByText('运行中')).not.toBeInTheDocument();
-    expect(screen.queryByText('只读')).not.toBeInTheDocument();
+    await screen.findAllByText('broker-a');
+    const runningTags = screen.getAllByText('运行中');
+    expect(runningTags.length).toBeGreaterThan(0);
+    const readonlyTags = screen.getAllByText('只读');
+    expect(readonlyTags.length).toBeGreaterThan(0);
   });
 
   it('should switch to NameServer tab on click', async () => {
     const user = userEvent.setup();
     renderWithProviders(<BrokerCluster />);
+    await screen.findByText('broker-a');
     const nsTab = screen.getByText('NameServer 管理');
     await user.click(nsTab);
-    expect(
-      screen.getAllByText('当前版本尚未接入真实集群拓扑接口,已停止展示模拟 Broker / NameServer / 
Proxy 数据。').length,
-    ).toBeGreaterThan(0);
-    expect(screen.queryByText('nameserver-a')).not.toBeInTheDocument();
+    // After clicking, NameServer data should be visible (name equals address, 
so it appears twice)
+    expect(screen.getAllByText('nameserver-a').length).toBeGreaterThan(0);
   });
 
   it('should switch to Proxy tab on click', async () => {
     const user = userEvent.setup();
     renderWithProviders(<BrokerCluster />);
+    await screen.findByText('broker-a');
     const proxyTab = screen.getByText('Proxy 管理');
     await user.click(proxyTab);
-    expect(
-      screen.getAllByText('当前版本尚未接入真实集群拓扑接口,已停止展示模拟 Broker / NameServer / 
Proxy 数据。').length,
-    ).toBeGreaterThan(0);
-    expect(screen.queryByText('proxy-a')).not.toBeInTheDocument();
+    // After clicking, Proxy data should be visible (proxy name equals its 
address, so it appears twice)
+    expect(screen.getAllByText('10.0.1.30:8080').length).toBeGreaterThan(0);
+  });
+
+  it('should render config and restart action buttons', async () => {
+    renderWithProviders(<BrokerCluster />);
+    await screen.findByText('broker-a');
+    const configButtons = screen.getAllByText('配置');
+    expect(configButtons.length).toBeGreaterThan(0);
+    const restartButtons = screen.getAllByText('重启');
+    expect(restartButtons.length).toBeGreaterThan(0);
   });
 
-  it('should not render row action buttons without real infrastructure data', 
() => {
+  it('should fall back to mock data when the API fails', async () => {
+    vi.mocked(listClusters).mockRejectedValueOnce(new Error('network error'));
     renderWithProviders(<BrokerCluster />);
-    expect(screen.queryByText('配置')).not.toBeInTheDocument();
-    expect(screen.queryByText('重启')).not.toBeInTheDocument();
+    // Initial state holds the mock fallback rows
+    await waitFor(() => {
+      expect(screen.getByText('broker-a')).toBeInTheDocument();
+    });
   });
 });
diff --git a/web/src/pages/studio/__tests__/GroupManagement.test.tsx 
b/web/src/pages/studio/__tests__/GroupManagement.test.tsx
index 05d48695..8db04717 100644
--- a/web/src/pages/studio/__tests__/GroupManagement.test.tsx
+++ b/web/src/pages/studio/__tests__/GroupManagement.test.tsx
@@ -15,13 +15,21 @@
  * limitations under the License.
  */
 
-import { describe, it, expect, vi, beforeAll } from 'vitest';
-import { render, screen } from '@testing-library/react';
+import { describe, it, expect, vi, beforeAll, beforeEach } from 'vitest';
+import { render, screen, waitFor } from '@testing-library/react';
 import userEvent from '@testing-library/user-event';
 import { App } from 'antd';
 import { LangProvider } from '../../../i18n/LangContext';
+import type { ConsumerGroup } from '../../../api/metadata';
+import * as consumerService from '../../../services/consumerService';
 import GroupManagement from '../GroupManagement';
 
+vi.mock('../../../services/consumerService', () => ({
+  listConsumerGroups: vi.fn(),
+  getConsumerProgress: vi.fn(),
+  getConsumerSubscriptions: vi.fn(),
+}));
+
 // Mock matchMedia for antd responsive components
 beforeAll(() => {
   Object.defineProperty(window, 'matchMedia', {
@@ -45,6 +53,29 @@ vi.mock('react-router-dom', () => ({
   useParams: () => ({}),
 }));
 
+const makeGroup = (overrides: Partial<ConsumerGroup>): ConsumerGroup => ({
+  name: 'order-consumer-group',
+  namespace: 'default',
+  clusterId: 'cluster-production',
+  subscriptionMode: 'Push',
+  consumeType: 'CLUSTERING',
+  onlineInstances: 4,
+  totalLag: 1280,
+  subscribedTopics: ['ORDER_TOPIC'],
+  subscriptionDataType: 'NORMAL',
+  retryMaxTimes: 16,
+  createdAt: '2025-03-15 10:30:00',
+  updatedAt: '2025-03-15 10:30:00',
+  delaySeconds: 12,
+  instances: [],
+  ...overrides,
+});
+
+const groups: ConsumerGroup[] = [
+  makeGroup({ name: 'order-consumer-group' }),
+  makeGroup({ name: 'payment-consumer-group', totalLag: 0, onlineInstances: 2 
}),
+];
+
 const renderWithProviders = (ui: React.ReactElement) => {
   return render(
     <App>
@@ -54,6 +85,12 @@ const renderWithProviders = (ui: React.ReactElement) => {
 };
 
 describe('GroupManagement Page', () => {
+  beforeEach(() => {
+    vi.mocked(consumerService.listConsumerGroups).mockResolvedValue(groups);
+    vi.mocked(consumerService.getConsumerProgress).mockResolvedValue([]);
+    vi.mocked(consumerService.getConsumerSubscriptions).mockResolvedValue([]);
+  });
+
   it('should render the page title', () => {
     renderWithProviders(<GroupManagement />);
     expect(screen.getByText('消费组管理')).toBeInTheDocument();
@@ -74,25 +111,32 @@ describe('GroupManagement Page', () => {
     expect(screen.getByText('重置')).toBeInTheDocument();
   });
 
-  it('should show an explicit unavailable state instead of mock consumer 
groups', () => {
+  it('should display consumer group data from the service in table', async () 
=> {
     renderWithProviders(<GroupManagement />);
-    
expect(screen.getByText('当前版本尚未接入真实消费组管理接口,已停止展示模拟消费组数据。')).toBeInTheDocument();
-    expect(screen.queryByText('order-consumer-group')).not.toBeInTheDocument();
-    
expect(screen.queryByText('payment-consumer-group')).not.toBeInTheDocument();
+    await waitFor(() => {
+      expect(screen.getByText('order-consumer-group')).toBeInTheDocument();
+    });
+    expect(screen.getByText('payment-consumer-group')).toBeInTheDocument();
   });
 
-  it('should not render row actions without real consumer group data', () => {
+  it('should render detail action buttons for each row', async () => {
     renderWithProviders(<GroupManagement />);
-    expect(screen.queryByText('详情')).not.toBeInTheDocument();
+    await waitFor(() => {
+      expect(screen.getByText('order-consumer-group')).toBeInTheDocument();
+    });
+    const detailButtons = screen.getAllByText('详情');
+    expect(detailButtons.length).toBeGreaterThan(0);
   });
 
-  it('should keep mock groups hidden when filtering by search text', async () 
=> {
+  it('should filter groups by search text', async () => {
     const user = userEvent.setup();
     renderWithProviders(<GroupManagement />);
+    await waitFor(() => {
+      expect(screen.getByText('order-consumer-group')).toBeInTheDocument();
+    });
     const searchInput = screen.getByPlaceholderText('搜索消费组');
     await user.type(searchInput, 'ORDER');
-    
expect(screen.getByText('当前版本尚未接入真实消费组管理接口,已停止展示模拟消费组数据。')).toBeInTheDocument();
-    expect(screen.queryByText('order-consumer-group')).not.toBeInTheDocument();
+    expect(screen.getByText('order-consumer-group')).toBeInTheDocument();
     
expect(screen.queryByText('payment-consumer-group')).not.toBeInTheDocument();
   });
 });
diff --git a/web/src/services/aclService.test.ts 
b/web/src/services/aclService.test.ts
index dea835f5..feaee9e3 100644
--- a/web/src/services/aclService.test.ts
+++ b/web/src/services/aclService.test.ts
@@ -25,9 +25,9 @@ import {
   updateAclUser,
 } from './aclService';
 
+vi.mock('./dataMode', () => ({ isMockMode: () => true }));
 vi.mock('../config', () => ({
   API_BASE_URL: '/api',
-  USE_MOCK: true,
 }));
 
 describe('ACL service mock data', () => {
diff --git a/web/src/services/aclService.ts b/web/src/services/aclService.ts
index df7361a7..29d56782 100644
--- a/web/src/services/aclService.ts
+++ b/web/src/services/aclService.ts
@@ -1,4 +1,4 @@
-import { USE_MOCK } from '../config';
+import { isMockMode } from './dataMode';
 import * as aclApi from '../api/acl';
 import type { AclRule, AclRuleQuery, AclUser } from '../api/acl';
 import { aclRules as mockRules, aclUsers as mockUsers } from '../mock/acl';
@@ -21,7 +21,7 @@ function copyAclUser(user: AclUser): AclUser {
 }
 
 export async function listAclRules(params?: AclRuleQuery): Promise<AclRule[]> {
-  if (USE_MOCK) {
+  if (isMockMode()) {
     let result = [...aclRulesState];
     if (params?.principal) {
       const principal = params.principal.toLowerCase();
@@ -33,7 +33,7 @@ export async function listAclRules(params?: AclRuleQuery): 
Promise<AclRule[]> {
 }
 
 export async function listAclUsers(params?: { keyword?: string }): 
Promise<AclUser[]> {
-  if (USE_MOCK) {
+  if (isMockMode()) {
     let result = [...aclUsersState];
     if (params?.keyword) {
       const kw = params.keyword.toLowerCase();
@@ -45,7 +45,7 @@ export async function listAclUsers(params?: { keyword?: 
string }): Promise<AclUs
 }
 
 export async function createAclRule(data: Partial<AclRule>): Promise<AclRule> {
-  if (USE_MOCK) {
+  if (isMockMode()) {
     const rule: AclRule = {
       id: `acl-${Date.now()}`,
       principal: '',
@@ -66,7 +66,7 @@ export async function createAclRule(data: Partial<AclRule>): 
Promise<AclRule> {
 }
 
 export async function updateAclRule(data: Partial<AclRule>): Promise<AclRule> {
-  if (USE_MOCK) {
+  if (isMockMode()) {
     const idx = aclRulesState.findIndex((rule) => rule.id === data.id);
     if (idx < 0) throw new Error(`ACL rule not found: ${data.id}`);
     aclRulesState[idx] = {
@@ -80,7 +80,7 @@ export async function updateAclRule(data: Partial<AclRule>): 
Promise<AclRule> {
 }
 
 export async function deleteAclRule(id: string): Promise<void> {
-  if (USE_MOCK) {
+  if (isMockMode()) {
     const idx = aclRulesState.findIndex((rule) => rule.id === id);
     if (idx >= 0) aclRulesState.splice(idx, 1);
     return;
@@ -89,7 +89,7 @@ export async function deleteAclRule(id: string): 
Promise<void> {
 }
 
 export async function createAclUser(data: Partial<AclUser>): Promise<AclUser> {
-  if (USE_MOCK) {
+  if (isMockMode()) {
     const user: AclUser = {
       id: `user-${Date.now()}`,
       username: '',
@@ -107,7 +107,7 @@ export async function createAclUser(data: 
Partial<AclUser>): Promise<AclUser> {
 }
 
 export async function updateAclUser(data: Partial<AclUser>): Promise<AclUser> {
-  if (USE_MOCK) {
+  if (isMockMode()) {
     const idx = aclUsersState.findIndex((user) => user.id === data.id);
     if (idx < 0) throw new Error(`ACL user not found: ${data.id}`);
     aclUsersState[idx] = {
@@ -121,7 +121,7 @@ export async function updateAclUser(data: 
Partial<AclUser>): Promise<AclUser> {
 }
 
 export async function deleteAclUser(id: string): Promise<void> {
-  if (USE_MOCK) {
+  if (isMockMode()) {
     const idx = aclUsersState.findIndex((user) => user.id === id);
     if (idx >= 0) aclUsersState.splice(idx, 1);
     return;
diff --git a/web/src/services/clusterService.test.ts 
b/web/src/services/clusterService.test.ts
index 3e153313..555c45f3 100644
--- a/web/src/services/clusterService.test.ts
+++ b/web/src/services/clusterService.test.ts
@@ -17,8 +17,8 @@
 
 import { describe, expect, it, vi } from 'vitest';
 
+vi.mock('./dataMode', () => ({ isMockMode: () => true }));
 vi.mock('../config', () => ({
-  USE_MOCK: true,
   API_BASE_URL: '/api',
 }));
 
diff --git a/web/src/services/clusterService.ts 
b/web/src/services/clusterService.ts
index a0958d7b..de6677dd 100644
--- a/web/src/services/clusterService.ts
+++ b/web/src/services/clusterService.ts
@@ -1,4 +1,4 @@
-import { USE_MOCK } from '../config';
+import { isMockMode } from './dataMode';
 import * as clusterApi from '../api/cluster';
 import type { ClusterConfig, ClusterInfo, ClusterProbeResult, K8sCertInfo } 
from '../api/cluster';
 import clusters, { mockK8sCerts } from '../mock/clusters';
@@ -28,14 +28,14 @@ function copyCluster(cluster: ClusterInfo): ClusterInfo {
 }
 
 export async function listClusters(): Promise<ClusterInfo[]> {
-  if (USE_MOCK) {
+  if (isMockMode()) {
     return clusters.map(copyCluster);
   }
   return clusterApi.listClusters();
 }
 
 export async function testClusterConnection(namesrvAddr: string): 
Promise<ClusterProbeResult> {
-  if (USE_MOCK) {
+  if (isMockMode()) {
     const trimmed = namesrvAddr.trim();
     const cluster = clusters[0];
     const brokerNames = cluster ? cluster.brokers.map((broker) => broker.name) 
: [];
@@ -53,7 +53,7 @@ export async function testClusterConnection(namesrvAddr: 
string): Promise<Cluste
 }
 
 export async function getCluster(id: string): Promise<ClusterInfo> {
-  if (USE_MOCK) {
+  if (isMockMode()) {
     const cluster = clusters.find((item) => item.id === id);
     if (!cluster) throw new Error('Cluster not found');
     return copyCluster(cluster);
@@ -62,12 +62,12 @@ export async function getCluster(id: string): 
Promise<ClusterInfo> {
 }
 
 export async function listK8sCerts(): Promise<K8sCertInfo[]> {
-  if (USE_MOCK) return mockCertStore.map((cert) => ({ ...cert, san: 
[...cert.san] }));
+  if (isMockMode()) return mockCertStore.map((cert) => ({ ...cert, san: 
[...cert.san] }));
   return clusterApi.listK8sCerts();
 }
 
 export async function createK8sCert(data: Partial<K8sCertInfo>): 
Promise<K8sCertInfo> {
-  if (USE_MOCK) {
+  if (isMockMode()) {
     const now = new Date();
     const notAfter = new Date(now);
     notAfter.setFullYear(notAfter.getFullYear() + 1);
@@ -91,7 +91,7 @@ export async function createK8sCert(data: 
Partial<K8sCertInfo>): Promise<K8sCert
 }
 
 export async function updateK8sCert(data: Partial<K8sCertInfo>): 
Promise<K8sCertInfo> {
-  if (USE_MOCK) {
+  if (isMockMode()) {
     const existing = mockCertStore.find((cert) => cert.id === data.id);
     if (!existing) throw new Error(`Certificate not found: ${data.id}`);
     Object.assign(existing, data, { san: data.san ?? existing.san });
@@ -101,7 +101,7 @@ export async function updateK8sCert(data: 
Partial<K8sCertInfo>): Promise<K8sCert
 }
 
 export async function renewK8sCert(id: string): Promise<K8sCertInfo> {
-  if (USE_MOCK) {
+  if (isMockMode()) {
     const existing = mockCertStore.find((cert) => cert.id === id);
     if (!existing) throw new Error(`Certificate not found: ${id}`);
     const now = new Date();
@@ -119,7 +119,7 @@ export async function renewK8sCert(id: string): 
Promise<K8sCertInfo> {
 }
 
 export async function deleteK8sCert(id: string): Promise<void> {
-  if (USE_MOCK) {
+  if (isMockMode()) {
     const index = mockCertStore.findIndex((cert) => cert.id === id);
     if (index < 0) throw new Error(`Certificate not found: ${id}`);
     mockCertStore.splice(index, 1);
@@ -129,7 +129,7 @@ export async function deleteK8sCert(id: string): 
Promise<void> {
 }
 
 export async function updateClusterConfig(data: { id: string } & 
Partial<ClusterConfig>) {
-  if (USE_MOCK) {
+  if (isMockMode()) {
     const { id, ...config } = data;
     Object.assign(getMockCluster(id).config, config);
     return;
@@ -138,7 +138,7 @@ export async function updateClusterConfig(data: { id: 
string } & Partial<Cluster
 }
 
 export async function restartBroker(clusterId: string, brokerName: string) {
-  if (USE_MOCK) return { success: true, message: `Broker ${brokerName} 
restarted (mock)` };
+  if (isMockMode()) return { success: true, message: `Broker ${brokerName} 
restarted (mock)` };
   return clusterApi.restartBroker(clusterId, brokerName);
 }
 
@@ -149,7 +149,7 @@ function getMockCluster(clusterId: string) {
 }
 
 export async function restartNameServer(data: { clusterId: string; addr: 
string }): Promise<void> {
-  if (USE_MOCK) {
+  if (isMockMode()) {
     const nameServer = getMockCluster(data.clusterId).nameServers.find(
       (item) => item.addr === data.addr,
     );
@@ -165,7 +165,7 @@ export async function upgradeNameServer(data: {
   addr: string;
   version: string;
 }): Promise<void> {
-  if (USE_MOCK) {
+  if (isMockMode()) {
     const exists = getMockCluster(data.clusterId).nameServers.some(
       (item) => item.addr === data.addr,
     );
@@ -176,7 +176,7 @@ export async function upgradeNameServer(data: {
 }
 
 export async function deleteNameServer(data: { clusterId: string; addr: string 
}): Promise<void> {
-  if (USE_MOCK) {
+  if (isMockMode()) {
     const nameServers = getMockCluster(data.clusterId).nameServers;
     const index = nameServers.findIndex((item) => item.addr === data.addr);
     if (index < 0) throw new Error(`NameServer not found: ${data.addr}`);
@@ -187,7 +187,7 @@ export async function deleteNameServer(data: { clusterId: 
string; addr: string }
 }
 
 export async function createNameServer(data: { clusterId: string; addr: string 
}): Promise<void> {
-  if (USE_MOCK) {
+  if (isMockMode()) {
     const nameServers = getMockCluster(data.clusterId).nameServers;
     if (nameServers.some((item) => item.addr === data.addr)) {
       throw new Error(`NameServer already exists: ${data.addr}`);
@@ -203,7 +203,7 @@ export async function updateNameServer(data: {
   addr: string;
   newAddr?: string;
 }): Promise<void> {
-  if (USE_MOCK) {
+  if (isMockMode()) {
     const nameServer = getMockCluster(data.clusterId).nameServers.find(
       (item) => item.addr === data.addr,
     );
@@ -215,7 +215,7 @@ export async function updateNameServer(data: {
 }
 
 export async function restartProxy(data: { clusterId: string; addr: string }): 
Promise<void> {
-  if (USE_MOCK) {
+  if (isMockMode()) {
     const proxy = getMockCluster(data.clusterId).proxies.find((item) => 
item.addr === data.addr);
     if (!proxy) throw new Error(`Proxy not found: ${data.addr}`);
     proxy.status = 'healthy';
diff --git a/web/src/services/connectionsService.test.ts 
b/web/src/services/connectionsService.test.ts
index 5461a0a0..6c461108 100644
--- a/web/src/services/connectionsService.test.ts
+++ b/web/src/services/connectionsService.test.ts
@@ -17,8 +17,8 @@
 
 import { describe, expect, it, vi } from 'vitest';
 
+vi.mock('./dataMode', () => ({ isMockMode: () => true }));
 vi.mock('../config', () => ({
-  USE_MOCK: true,
   API_BASE_URL: '/api',
 }));
 
diff --git a/web/src/services/connectionsService.ts 
b/web/src/services/connectionsService.ts
index d29b58a0..d4adc728 100644
--- a/web/src/services/connectionsService.ts
+++ b/web/src/services/connectionsService.ts
@@ -1,4 +1,4 @@
-import { USE_MOCK } from '../config';
+import { isMockMode } from './dataMode';
 import * as connApi from '../api/connections';
 import type { ClientConnection, ClientConnectionQuery } from 
'../api/connections';
 import { mockClients } from '../mock/clients';
@@ -8,7 +8,7 @@ function copyConnection(connection: ClientConnection): 
ClientConnection {
 }
 
 export async function listConnections(params?: ClientConnectionQuery): 
Promise<ClientConnection[]> {
-  if (USE_MOCK) {
+  if (isMockMode()) {
     let result = [...mockClients];
     if (params?.clusterId)
       result = result.filter((connection) => connection.clusterName === 
params.clusterId);
diff --git a/web/src/services/consumerService.test.ts 
b/web/src/services/consumerService.test.ts
index 603861d1..d5d637cf 100644
--- a/web/src/services/consumerService.test.ts
+++ b/web/src/services/consumerService.test.ts
@@ -24,9 +24,9 @@ import {
   listConsumerGroups,
 } from './consumerService';
 
+vi.mock('./dataMode', () => ({ isMockMode: () => true }));
 vi.mock('../config', () => ({
   API_BASE_URL: '/api',
-  USE_MOCK: true,
 }));
 
 describe('consumer service mock data', () => {
diff --git a/web/src/services/consumerService.ts 
b/web/src/services/consumerService.ts
index c1859dce..6cfc0678 100644
--- a/web/src/services/consumerService.ts
+++ b/web/src/services/consumerService.ts
@@ -1,4 +1,4 @@
-import { USE_MOCK } from '../config';
+import { isMockMode } from './dataMode';
 import * as metadataApi from '../api/metadata';
 import type {
   ConsumerGroup,
@@ -12,7 +12,9 @@ import { mockConsumerGroups, mockQueueProgress, 
mockSubscriptions } from '../moc
 
 const consumerGroupsState = mockConsumerGroups as unknown as ConsumerGroup[];
 
-function copyConsumerInstance(instance: ConsumerGroup['instances'][number]): 
ConsumerGroup['instances'][number] {
+function copyConsumerInstance(
+  instance: ConsumerGroup['instances'][number],
+): ConsumerGroup['instances'][number] {
   return {
     ...instance,
     subscribedTopics: [...instance.subscribedTopics],
@@ -37,7 +39,7 @@ function copySubscription(subscription: SubscriptionEntry): 
SubscriptionEntry {
 }
 
 export async function listConsumerGroups(params?: ConsumerGroupQuery): 
Promise<ConsumerGroup[]> {
-  if (USE_MOCK) {
+  if (isMockMode()) {
     let result = [...consumerGroupsState];
     if (params?.clusterId) result = result.filter((group) => group.clusterId 
=== params.clusterId);
     if (params?.search) {
@@ -50,14 +52,14 @@ export async function listConsumerGroups(params?: 
ConsumerGroupQuery): Promise<C
 }
 
 export async function getConsumerProgress(name: string): 
Promise<QueueProgress[]> {
-  if (USE_MOCK) {
+  if (isMockMode()) {
     return ((mockQueueProgress[name] as unknown as QueueProgress[]) ?? 
[]).map(copyQueueProgress);
   }
   return metadataApi.getConsumerProgress(name);
 }
 
 export async function getConsumerGroup(name: string): 
Promise<ConsumerGroupDetail> {
-  if (USE_MOCK) {
+  if (isMockMode()) {
     const group = mockConsumerGroups.find((item) => item.name === name);
     if (!group) throw new Error(`Consumer group not found: ${name}`);
     return copyConsumerGroup(group as unknown as ConsumerGroupDetail) as 
ConsumerGroupDetail;
@@ -66,14 +68,16 @@ export async function getConsumerGroup(name: string): 
Promise<ConsumerGroupDetai
 }
 
 export async function getConsumerSubscriptions(name: string): 
Promise<SubscriptionEntry[]> {
-  if (USE_MOCK) {
-    return ((mockSubscriptions[name] as unknown as SubscriptionEntry[]) ?? 
[]).map(copySubscription);
+  if (isMockMode()) {
+    return ((mockSubscriptions[name] as unknown as SubscriptionEntry[]) ?? 
[]).map(
+      copySubscription,
+    );
   }
   return metadataApi.getConsumerSubscriptions(name);
 }
 
 export async function createConsumerGroup(data: Partial<ConsumerGroup>): 
Promise<ConsumerGroup> {
-  if (USE_MOCK) {
+  if (isMockMode()) {
     const now = new Date().toISOString();
     const group = {
       name: data.name ?? '',
@@ -98,7 +102,7 @@ export async function createConsumerGroup(data: 
Partial<ConsumerGroup>): Promise
 }
 
 export async function deleteConsumerGroup(name: string): Promise<void> {
-  if (USE_MOCK) {
+  if (isMockMode()) {
     const idx = consumerGroupsState.findIndex((group) => group.name === name);
     if (idx >= 0) consumerGroupsState.splice(idx, 1);
     return;
@@ -107,7 +111,7 @@ export async function deleteConsumerGroup(name: string): 
Promise<void> {
 }
 
 export async function resetConsumerOffset(data: ResetConsumerOffsetRequest): 
Promise<void> {
-  if (USE_MOCK) return;
+  if (isMockMode()) return;
   return metadataApi.resetConsumerOffset(data);
 }
 
diff --git a/web/src/services/dashboardService.test.ts 
b/web/src/services/dashboardService.test.ts
index f2bfc39b..2098a9ba 100644
--- a/web/src/services/dashboardService.test.ts
+++ b/web/src/services/dashboardService.test.ts
@@ -17,8 +17,8 @@
 
 import { describe, expect, it, vi } from 'vitest';
 
+vi.mock('./dataMode', () => ({ isMockMode: () => true }));
 vi.mock('../config', () => ({
-  USE_MOCK: true,
   API_BASE_URL: '/api',
 }));
 
diff --git a/web/src/services/dashboardService.ts 
b/web/src/services/dashboardService.ts
index 0d396eff..2cd749d3 100644
--- a/web/src/services/dashboardService.ts
+++ b/web/src/services/dashboardService.ts
@@ -1,4 +1,4 @@
-import { USE_MOCK } from '../config';
+import { isMockMode } from './dataMode';
 import * as metricsApi from '../api/metrics';
 import { dashboardStats, clusterOverview } from '../mock/dashboard';
 import type { DashboardData } from '../api/metrics';
@@ -11,7 +11,7 @@ function copyClusterOverview(cluster: 
DashboardData['clusters'][number]) {
 }
 
 export async function getDashboard(): Promise<DashboardData> {
-  if (USE_MOCK) {
+  if (isMockMode()) {
     return {
       stats: { ...dashboardStats },
       clusters: (clusterOverview as 
DashboardData['clusters']).map(copyClusterOverview),
diff --git a/web/src/services/dataMode.ts b/web/src/services/dataMode.ts
new file mode 100644
index 00000000..0c00d887
--- /dev/null
+++ b/web/src/services/dataMode.ts
@@ -0,0 +1,27 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+import { useDataModeStore } from '../stores/dataModeStore';
+
+/**
+ * Returns whether the app is currently in mock-data mode.
+ * Reads from the Zustand store (persisted to localStorage) so the value
+ * can be toggled at runtime without a rebuild.
+ */
+export function isMockMode(): boolean {
+  return useDataModeStore.getState().useMock;
+}
diff --git a/web/src/services/instanceService.test.ts 
b/web/src/services/instanceService.test.ts
index a8f93118..b003f31d 100644
--- a/web/src/services/instanceService.test.ts
+++ b/web/src/services/instanceService.test.ts
@@ -17,8 +17,8 @@
 
 import { describe, expect, it, vi } from 'vitest';
 
+vi.mock('./dataMode', () => ({ isMockMode: () => true }));
 vi.mock('../config', () => ({
-  USE_MOCK: true,
   API_BASE_URL: '/api',
 }));
 
diff --git a/web/src/services/instanceService.ts 
b/web/src/services/instanceService.ts
index 0b6564c4..d162c160 100644
--- a/web/src/services/instanceService.ts
+++ b/web/src/services/instanceService.ts
@@ -1,4 +1,4 @@
-import { USE_MOCK } from '../config';
+import { isMockMode } from './dataMode';
 import * as instanceApi from '../api/instance';
 import type {
   Instance,
@@ -9,14 +9,13 @@ import type {
 import { mockInstances } from '../mock/instances';
 
 // Compile-time switch: mock or real API
-// Vite replaces USE_MOCK with a literal at build time → tree-shaking removes 
unused branch
 
 function copyInstance(instance: Instance): Instance {
   return { ...instance };
 }
 
 export async function listInstances(query: InstanceQuery = {}): 
Promise<Instance[]> {
-  if (USE_MOCK) {
+  if (isMockMode()) {
     const search = query.search?.trim().toLowerCase();
     return mockInstances
       .filter((instance) => !query.type || instance.type === query.type)
@@ -33,7 +32,7 @@ export async function listInstances(query: InstanceQuery = 
{}): Promise<Instance
 }
 
 export async function createInstance(data: CreateInstanceRequest): 
Promise<Instance> {
-  if (USE_MOCK) {
+  if (isMockMode()) {
     const instance: Instance = {
       id: String(Date.now()),
       ...data,
@@ -50,7 +49,7 @@ export async function createInstance(data: 
CreateInstanceRequest): Promise<Insta
 }
 
 export async function updateInstance(data: UpdateInstanceRequest): 
Promise<Instance> {
-  if (USE_MOCK) {
+  if (isMockMode()) {
     const idx = mockInstances.findIndex((i) => i.id === data.id);
     if (idx >= 0) {
       Object.assign(mockInstances[idx], data, {
@@ -64,7 +63,7 @@ export async function updateInstance(data: 
UpdateInstanceRequest): Promise<Insta
 }
 
 export async function deleteInstance(id: string): Promise<void> {
-  if (USE_MOCK) {
+  if (isMockMode()) {
     const idx = mockInstances.findIndex((i) => i.id === id);
     if (idx >= 0) mockInstances.splice(idx, 1);
     return;
diff --git a/web/src/services/messageService.test.ts 
b/web/src/services/messageService.test.ts
index 42b0c926..3e010f01 100644
--- a/web/src/services/messageService.test.ts
+++ b/web/src/services/messageService.test.ts
@@ -18,9 +18,9 @@
 import { describe, expect, it, vi } from 'vitest';
 import { getMessageTrace, listDLQGroups, queryMessages } from 
'./messageService';
 
+vi.mock('./dataMode', () => ({ isMockMode: () => true }));
 vi.mock('../config', () => ({
   API_BASE_URL: '/api',
-  USE_MOCK: true,
 }));
 
 describe('message service mock data', () => {
diff --git a/web/src/services/messageService.ts 
b/web/src/services/messageService.ts
index 750afc21..44d74c27 100644
--- a/web/src/services/messageService.ts
+++ b/web/src/services/messageService.ts
@@ -1,4 +1,4 @@
-import { USE_MOCK } from '../config';
+import { isMockMode } from './dataMode';
 import * as messageApi from '../api/message';
 import { sortMessagesByStoreTimeDesc } from '../api/message';
 import type { MessageQuery, MessageRecord, TraceRecord, DLQGroup } from 
'../api/message';
@@ -18,7 +18,7 @@ const cloneTrace = (trace: TraceRecord): TraceRecord => ({
 const cloneDLQGroup = (group: DLQGroup): DLQGroup => ({ ...group });
 
 export async function queryMessages(params: MessageQuery): 
Promise<MessageRecord[]> {
-  if (USE_MOCK) {
+  if (isMockMode()) {
     let result = [...mockMessages];
     if (params.topic) result = result.filter((m) => m.topic === params.topic);
     if (params.tag) result = result.filter((m) => m.tag === params.tag);
@@ -30,7 +30,7 @@ export async function queryMessages(params: MessageQuery): 
Promise<MessageRecord
 }
 
 export async function getMessageTrace(msgId: string): Promise<TraceRecord | 
null> {
-  if (USE_MOCK) {
+  if (isMockMode()) {
     const trace = mockMessageTraces[msgId] as unknown as TraceRecord | 
undefined;
     return trace ? cloneTrace(trace) : null;
   }
@@ -38,7 +38,7 @@ export async function getMessageTrace(msgId: string): 
Promise<TraceRecord | null
 }
 
 export async function listDLQGroups(): Promise<DLQGroup[]> {
-  if (USE_MOCK) return (mockDLQGroups as unknown as 
DLQGroup[]).map(cloneDLQGroup);
+  if (isMockMode()) return (mockDLQGroups as unknown as 
DLQGroup[]).map(cloneDLQGroup);
   return messageApi.listDLQGroups();
 }
 
@@ -48,6 +48,6 @@ export async function resendDLQ(data: {
   endTime: number;
   targetTopic?: string;
 }): Promise<void> {
-  if (USE_MOCK) return;
+  if (isMockMode()) return;
   return messageApi.resendDLQ(data);
 }
diff --git a/web/src/services/opsService.test.ts 
b/web/src/services/opsService.test.ts
index 2571a12b..ae02a2dc 100644
--- a/web/src/services/opsService.test.ts
+++ b/web/src/services/opsService.test.ts
@@ -28,9 +28,9 @@ import {
   updateAlertRule,
 } from './opsService';
 
+vi.mock('./dataMode', () => ({ isMockMode: () => true }));
 vi.mock('../config', () => ({
   API_BASE_URL: '/api',
-  USE_MOCK: true,
 }));
 
 describe('ops service mock data', () => {
diff --git a/web/src/services/opsService.ts b/web/src/services/opsService.ts
index 9750fc1b..b27819ad 100644
--- a/web/src/services/opsService.ts
+++ b/web/src/services/opsService.ts
@@ -1,6 +1,6 @@
-import { USE_MOCK } from '../config';
 import { exportAuditLogs as exportAuditLogsApi } from '../api/audit';
 import type { AuditFilter } from '../api/audit';
+import { isMockMode } from './dataMode';
 import * as opsApi from '../api/ops';
 import type { AlertRule, SystemAlert, AuditQuery, AuditRecord, PageResult } 
from '../api/ops';
 import { mockAlertRules } from '../mock/alerts';
@@ -72,12 +72,12 @@ function formatAuditCsv(records: AuditRecord[]): string {
 }
 
 export async function listAlertRules(): Promise<AlertRule[]> {
-  if (USE_MOCK) return alertRulesState.map(copyAlertRule);
+  if (isMockMode()) return alertRulesState.map(copyAlertRule);
   return opsApi.listAlertRules();
 }
 
 export async function createAlertRule(data: Partial<AlertRule>): 
Promise<AlertRule> {
-  if (USE_MOCK) {
+  if (isMockMode()) {
     const rule: AlertRule = {
       id: `alert-${Date.now()}`,
       name: '',
@@ -99,7 +99,7 @@ export async function createAlertRule(data: 
Partial<AlertRule>): Promise<AlertRu
 }
 
 export async function updateAlertRule(data: AlertRule): Promise<AlertRule> {
-  if (USE_MOCK) {
+  if (isMockMode()) {
     const index = alertRulesState.findIndex((rule) => rule.id === data.id);
     const rule = copyAlertRule(data);
     if (index >= 0) alertRulesState[index] = rule;
@@ -109,7 +109,7 @@ export async function updateAlertRule(data: AlertRule): 
Promise<AlertRule> {
 }
 
 export async function toggleAlertRule(id: string, enabled: boolean): 
Promise<AlertRule> {
-  if (USE_MOCK) {
+  if (isMockMode()) {
     const rule = alertRulesState.find((item) => item.id === id);
     if (!rule) throw new Error(`Alert rule not found: ${id}`);
     rule.enabled = enabled;
@@ -119,7 +119,7 @@ export async function toggleAlertRule(id: string, enabled: 
boolean): Promise<Ale
 }
 
 export async function deleteAlertRule(id: string): Promise<void> {
-  if (USE_MOCK) {
+  if (isMockMode()) {
     const idx = alertRulesState.findIndex((rule) => rule.id === id);
     if (idx >= 0) alertRulesState.splice(idx, 1);
     return;
@@ -128,12 +128,12 @@ export async function deleteAlertRule(id: string): 
Promise<void> {
 }
 
 export async function listSystemAlerts(): Promise<SystemAlert[]> {
-  if (USE_MOCK) return (mockSystemAlerts as unknown as 
SystemAlert[]).map(copySystemAlert);
+  if (isMockMode()) return (mockSystemAlerts as unknown as 
SystemAlert[]).map(copySystemAlert);
   return opsApi.listSystemAlerts();
 }
 
 export async function acknowledgeAlert(id: string): Promise<void> {
-  if (USE_MOCK) {
+  if (isMockMode()) {
     const a = mockSystemAlerts.find((a: Record<string, unknown>) => a.id === 
id);
     if (a) (a as Record<string, unknown>).acknowledged = true;
     return;
@@ -142,7 +142,7 @@ export async function acknowledgeAlert(id: string): 
Promise<void> {
 }
 
 export async function clearAcknowledgedAlerts(): Promise<number> {
-  if (USE_MOCK) {
+  if (isMockMode()) {
     const acknowledged = mockSystemAlerts.filter((alert) => 
alert.acknowledged).length;
     const remaining = mockSystemAlerts.filter((alert) => !alert.acknowledged);
     mockSystemAlerts.splice(0, mockSystemAlerts.length, ...remaining);
@@ -153,7 +153,7 @@ export async function clearAcknowledgedAlerts(): 
Promise<number> {
 }
 
 export async function listAuditRecords(params: AuditQuery = {}): 
Promise<PageResult<AuditRecord>> {
-  if (!USE_MOCK) return opsApi.listAuditRecords(params);
+  if (!isMockMode()) return opsApi.listAuditRecords(params);
 
   const page = params.page ?? 1;
   const pageSize = params.pageSize ?? 20;
@@ -168,12 +168,12 @@ export async function listAuditRecords(params: AuditQuery 
= {}): Promise<PageRes
 }
 
 export async function exportAuditLogs(params: AuditFilter = {}): 
Promise<string> {
-  if (!USE_MOCK) return exportAuditLogsApi(params);
+  if (!isMockMode()) return exportAuditLogsApi(params);
   return formatAuditCsv(filterAuditRecords(params));
 }
 
 export async function cleanupAuditLogs(beforeDays: number): Promise<number> {
-  if (USE_MOCK) {
+  if (isMockMode()) {
     const cutoff = new Date(Date.now() - beforeDays * 24 * 60 * 60 * 1000);
     const remaining = auditRecordsState.filter((record) => new 
Date(record.timestamp) >= cutoff);
     const deleted = auditRecordsState.length - remaining.length;
diff --git a/web/src/services/topicService.test.ts 
b/web/src/services/topicService.test.ts
index ff832977..04580f07 100644
--- a/web/src/services/topicService.test.ts
+++ b/web/src/services/topicService.test.ts
@@ -17,9 +17,9 @@
 
 import { getTopicConsumers, getTopicRoutes, listTopics } from './topicService';
 
+vi.mock('./dataMode', () => ({ isMockMode: () => true }));
 vi.mock('../config', () => ({
   API_BASE_URL: '/api',
-  USE_MOCK: true,
 }));
 
 describe('topic service mock data', () => {
diff --git a/web/src/services/topicService.ts b/web/src/services/topicService.ts
index 5db5ac48..ca1e945a 100644
--- a/web/src/services/topicService.ts
+++ b/web/src/services/topicService.ts
@@ -1,4 +1,4 @@
-import { USE_MOCK } from '../config';
+import { isMockMode } from './dataMode';
 import * as metadataApi from '../api/metadata';
 import type {
   Topic,
@@ -16,7 +16,7 @@ const cloneConsumers = (consumers: ConsumerGroupInfo[]): 
ConsumerGroupInfo[] =>
   consumers.map((consumer) => ({ ...consumer }));
 
 export async function listTopics(params?: TopicQuery): Promise<Topic[]> {
-  if (USE_MOCK) {
+  if (isMockMode()) {
     let result = [...mockTopics];
     if (params?.search) {
       const keyword = params.search.trim().toLowerCase();
@@ -30,7 +30,7 @@ export async function listTopics(params?: TopicQuery): 
Promise<Topic[]> {
 }
 
 export async function createTopic(data: Partial<Topic>): Promise<Topic> {
-  if (USE_MOCK) {
+  if (isMockMode()) {
     const topic = {
       ...data,
       createdAt: new Date().toISOString(),
@@ -46,7 +46,7 @@ export async function createTopic(data: Partial<Topic>): 
Promise<Topic> {
 }
 
 export async function updateTopic(data: Partial<Topic>): Promise<Topic> {
-  if (USE_MOCK) {
+  if (isMockMode()) {
     const idx = mockTopics.findIndex((t) => t.name === data.name);
     if (idx < 0) throw new Error(`Topic not found: ${data.name}`);
     Object.assign(mockTopics[idx], data, { updatedAt: new Date().toISOString() 
});
@@ -56,7 +56,7 @@ export async function updateTopic(data: Partial<Topic>): 
Promise<Topic> {
 }
 
 export async function deleteTopic(name: string): Promise<void> {
-  if (USE_MOCK) {
+  if (isMockMode()) {
     const idx = mockTopics.findIndex((t) => t.name === name);
     if (idx >= 0) mockTopics.splice(idx, 1);
     return;
@@ -84,12 +84,12 @@ export async function batchDeleteTopics(names: string[]): 
Promise<BatchDeleteTop
 }
 
 export async function getTopicRoutes(name: string): Promise<BrokerRoute[]> {
-  if (USE_MOCK) return cloneRoutes((topicRoutes[name] as unknown as 
BrokerRoute[]) ?? []);
+  if (isMockMode()) return cloneRoutes((topicRoutes[name] as unknown as 
BrokerRoute[]) ?? []);
   return metadataApi.getTopicRoutes(name);
 }
 
 export async function getTopicConsumers(name: string): 
Promise<ConsumerGroupInfo[]> {
-  if (USE_MOCK)
+  if (isMockMode())
     return cloneConsumers((topicConsumers[name] as unknown as 
ConsumerGroupInfo[]) ?? []);
   return metadataApi.getTopicConsumers(name);
 }
@@ -97,7 +97,7 @@ export async function getTopicConsumers(name: string): 
Promise<ConsumerGroupInfo
 export async function sendTopicMessage(
   data: SendTopicMessageRequest,
 ): Promise<SendTopicMessageResult> {
-  if (USE_MOCK) {
+  if (isMockMode()) {
     // Simulate a short delay
     await new Promise((r) => setTimeout(r, 400));
     return {
diff --git a/web/src/stores/dataModeStore.ts b/web/src/stores/dataModeStore.ts
new file mode 100644
index 00000000..195c40be
--- /dev/null
+++ b/web/src/stores/dataModeStore.ts
@@ -0,0 +1,36 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+import { create } from 'zustand';
+import { persist } from 'zustand/middleware';
+
+interface DataModeState {
+  useMock: boolean;
+  toggle: () => void;
+}
+
+export const useDataModeStore = create<DataModeState>()(
+  persist(
+    (set) => ({
+      useMock: false,
+      toggle: () => set((state) => ({ useMock: !state.useMock })),
+    }),
+    {
+      name: 'rocketmq-studio-data-mode',
+    },
+  ),
+);

Reply via email to