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

commit 7927a09e4cee2f13f2f678695b4463ab382d99f7
Author: zhaohai <[email protected]>
AuthorDate: Wed Jul 22 19:22:36 2026 +0800

    feat: add broker cluster overview page (#479)
    
    Add Broker Cluster overview page with tab-based NameServer/Broker/Proxy 
management and cluster stats.
---
 web/src/App.tsx                                    |   2 +
 web/src/i18n/translations.ts                       |  25 +
 web/src/pages/studio/BrokerCluster.tsx             | 551 +++++++++++++++++++++
 .../pages/studio/__tests__/BrokerCluster.test.tsx  | 112 +++++
 4 files changed, 690 insertions(+)

diff --git a/web/src/App.tsx b/web/src/App.tsx
index 5ace3a4e..f4ca3341 100644
--- a/web/src/App.tsx
+++ b/web/src/App.tsx
@@ -33,6 +33,7 @@ import SystemAlertsPage from './pages/ops/systemAlerts';
 import AuditPage from './pages/ops/audit';
 import AiPage from './pages/ai';
 import SettingsPage from './pages/settings';
+import BrokerClusterPage from './pages/studio/BrokerCluster';
 import SslSettingsPage from './pages/studio/SslSettings';
 import AlertManagementPage from './pages/studio/AlertManagement';
 import ProducerPage from './pages/studio/Producer';
@@ -60,6 +61,7 @@ function App() {
         <Route path="ops/audit" element={<AuditPage />} />
         <Route path="ai" element={<AiPage />} />
         <Route path="settings" element={<SettingsPage />} />
+        <Route path="studio/broker-cluster" element={<BrokerClusterPage />} />
         <Route path="studio/ssl-settings" element={<SslSettingsPage />} />
         <Route path="studio/alert-management" element={<AlertManagementPage 
/>} />
         <Route path="studio/producer" element={<ProducerPage />} />
diff --git a/web/src/i18n/translations.ts b/web/src/i18n/translations.ts
index 7928b720..d14c1fd1 100644
--- a/web/src/i18n/translations.ts
+++ b/web/src/i18n/translations.ts
@@ -875,6 +875,31 @@ const translations: Record<string, Record<Lang, string>> = 
{
   'common.warning': { zh: '警告', en: 'Warning' },
   'common.na': { zh: 'N/A', en: 'N/A' },
   'common.default': { zh: '默认', en: 'Default' },
+
+  // ─── BrokerCluster ───
+  'brokerCluster.title': { zh: 'RocketMQ 集群', en: 'RocketMQ Cluster' },
+  'brokerCluster.k8sCluster': { zh: 'K8s 集群名称', en: 'K8s Cluster' },
+  'brokerCluster.brokerName': { zh: 'Broker 名称', en: 'Broker Name' },
+  'brokerCluster.status': { zh: '运行状态', en: 'Status' },
+  'brokerCluster.statusRunning': { zh: '运行中', en: 'Running' },
+  'brokerCluster.statusReadonly': { zh: '只读', en: 'Readonly' },
+  'brokerCluster.statusMaintenance': { zh: '维护中', en: 'Maintenance' },
+  'brokerCluster.version': { zh: '版本', en: 'Version' },
+  'brokerCluster.diskUsage': { zh: '磁盘使用率', en: 'Disk Usage' },
+  'brokerCluster.tpsIn': { zh: 'TPS 入流量', en: 'TPS In' },
+  'brokerCluster.tpsOut': { zh: 'TPS 出流量', en: 'TPS Out' },
+  'brokerCluster.config': { zh: '配置', en: 'Config' },
+  'brokerCluster.restart': { zh: '重启', en: 'Restart' },
+  'brokerCluster.manual': { zh: '手动', en: 'Manual' },
+  'brokerCluster.createCluster': { zh: '新建集群', en: 'Create Cluster' },
+  'brokerCluster.nsName': { zh: 'NameServer 名称', en: 'NameServer Name' },
+  'brokerCluster.nsManagement': { zh: 'NameServer 管理', en: 'NameServer 
Management' },
+  'brokerCluster.brokerManagement': { zh: 'Broker 管理', en: 'Broker Management' 
},
+  'brokerCluster.proxyName': { zh: 'Proxy 名称', en: 'Proxy Name' },
+  'brokerCluster.proxyManagement': { zh: 'Proxy 管理', en: 'Proxy Management' },
+  'brokerCluster.httpAddr': { zh: 'HTTP 地址', en: 'HTTP Address' },
+  'brokerCluster.grpcAddr': { zh: 'gRPC 地址', en: 'gRPC Address' },
+  'brokerCluster.connections': { zh: '连接数', en: 'Connections' },
 };
 
 export default translations;
diff --git a/web/src/pages/studio/BrokerCluster.tsx 
b/web/src/pages/studio/BrokerCluster.tsx
new file mode 100644
index 00000000..ab21b02c
--- /dev/null
+++ b/web/src/pages/studio/BrokerCluster.tsx
@@ -0,0 +1,551 @@
+/*
+ * 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 { useState } from 'react';
+import { Table, Button, Tag, Tabs, Card, Space, Switch, Progress, Tooltip } 
from 'antd';
+import {
+  Plus,
+  ArrowClockwise,
+  GearSix,
+  ArrowsClockwise,
+  Cloud,
+  ChartBar,
+  PlugsConnected,
+} from '@phosphor-icons/react';
+import { useLang } from '../../i18n/LangContext';
+
+// ─── Types ──────────────────────────────────────────────────────
+interface BrokerRecord {
+  key: string;
+  k8sCluster: string;
+  brokerName: string;
+  status: 'running' | 'readonly' | 'maintenance';
+  version: string;
+  diskUsage: number;
+  address: string;
+  tpsIn: string;
+  tpsOut: string;
+}
+
+interface NameServerRecord {
+  key: string;
+  k8sCluster: string;
+  name: string;
+  status: 'running' | 'readonly' | 'maintenance';
+  version: string;
+  address: string;
+  connections: number;
+}
+
+interface ProxyRecord {
+  key: string;
+  k8sCluster: string;
+  name: string;
+  status: 'running' | 'readonly' | 'maintenance';
+  version: string;
+  address: string;
+  grpcPort: string;
+  connections: number;
+}
+
+// ─── Mock Data ──────────────────────────────────────────────────
+const brokerData: 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: '12,580',
+    tpsOut: '8,340',
+  },
+  {
+    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: '3,120',
+  },
+  {
+    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: '9,750',
+    tpsOut: '6,280',
+  },
+  {
+    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: '7,890',
+    tpsOut: '5,430',
+  },
+  {
+    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: '1,230',
+    tpsOut: '980',
+  },
+];
+
+const nameServerData: 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 proxyData: 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,
+  },
+];
+
+// ─── Component ──────────────────────────────────────────────────
+const BrokerClusterPage = () => {
+  const [autoRefresh, setAutoRefresh] = useState(false);
+  const [activeTab, setActiveTab] = useState('broker');
+  const { t } = useLang();
+
+  const renderStatus = (status: string) => {
+    const config: Record<string, { color: string; label: string }> = {
+      running: { color: 'success', label: t('brokerCluster.statusRunning') },
+      readonly: { color: 'warning', label: t('brokerCluster.statusReadonly') },
+      maintenance: {
+        color: 'error',
+        label: t('brokerCluster.statusMaintenance'),
+      },
+    };
+    const { color, label } = config[status] || config.running;
+    return <Tag color={color}>{label}</Tag>;
+  };
+
+  const renderDiskUsage = (percent: number) => {
+    let status: 'normal' | 'active' | 'exception' = 'normal';
+    let color = '#52c41a';
+    if (percent > 85) {
+      status = 'exception';
+      color = '#ff4d4f';
+    } else if (percent > 70) {
+      status = 'active';
+      color = '#fa8c16';
+    }
+    return (
+      <div style={{ display: 'flex', alignItems: 'center', gap: 8 }}>
+        <Progress
+          percent={percent}
+          size="small"
+          status={status}
+          style={{ width: 80, margin: 0 }}
+          strokeColor={color}
+        />
+        <span style={{ fontSize: 12, color, fontWeight: 500 
}}>{percent}%</span>
+      </div>
+    );
+  };
+
+  const brokerColumns = [
+    {
+      title: t('brokerCluster.k8sCluster'),
+      dataIndex: 'k8sCluster',
+      key: 'k8sCluster',
+      render: (text: string) => <span style={{ fontWeight: 500 
}}>{text}</span>,
+    },
+    {
+      title: t('brokerCluster.brokerName'),
+      dataIndex: 'brokerName',
+      key: 'brokerName',
+      render: (text: string) => <span style={{ color: '#1677ff', fontWeight: 
500 }}>{text}</span>,
+    },
+    {
+      title: t('brokerCluster.status'),
+      dataIndex: 'status',
+      key: 'status',
+      render: renderStatus,
+    },
+    { title: t('brokerCluster.version'), dataIndex: 'version', key: 'version' 
},
+    {
+      title: t('brokerCluster.diskUsage'),
+      dataIndex: 'diskUsage',
+      key: 'diskUsage',
+      render: renderDiskUsage,
+      width: 160,
+    },
+    {
+      title: t('common.address'),
+      dataIndex: 'address',
+      key: 'address',
+      render: (text: string) => (
+        <code
+          style={{
+            fontSize: 12,
+            background: '#f5f5f5',
+            padding: '2px 6px',
+            borderRadius: 4,
+          }}
+        >
+          {text}
+        </code>
+      ),
+    },
+    {
+      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, '')),
+    },
+    {
+      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, '')),
+    },
+    {
+      title: t('common.actions'),
+      key: 'action',
+      render: () => (
+        <Space size="small">
+          <Tooltip title={t('brokerCluster.config')}>
+            <Button type="link" size="small" icon={<GearSix size={14} />}>
+              {t('brokerCluster.config')}
+            </Button>
+          </Tooltip>
+          <Tooltip title={t('brokerCluster.restart')}>
+            <Button type="link" size="small" icon={<ArrowsClockwise size={14} 
/>}>
+              {t('brokerCluster.restart')}
+            </Button>
+          </Tooltip>
+        </Space>
+      ),
+    },
+  ];
+
+  const nsColumns = [
+    {
+      title: t('brokerCluster.k8sCluster'),
+      dataIndex: 'k8sCluster',
+      key: 'k8sCluster',
+      render: (text: string) => <span style={{ fontWeight: 500 
}}>{text}</span>,
+    },
+    {
+      title: t('brokerCluster.nsName'),
+      dataIndex: 'name',
+      key: 'name',
+      render: (text: string) => <span style={{ color: '#1677ff', fontWeight: 
500 }}>{text}</span>,
+    },
+    {
+      title: t('brokerCluster.status'),
+      dataIndex: 'status',
+      key: 'status',
+      render: renderStatus,
+    },
+    { title: t('brokerCluster.version'), dataIndex: 'version', key: 'version' 
},
+    {
+      title: t('common.address'),
+      dataIndex: 'address',
+      key: 'address',
+      render: (text: string) => (
+        <code
+          style={{
+            fontSize: 12,
+            background: '#f5f5f5',
+            padding: '2px 6px',
+            borderRadius: 4,
+          }}
+        >
+          {text}
+        </code>
+      ),
+    },
+    {
+      title: t('brokerCluster.connections'),
+      dataIndex: 'connections',
+      key: 'connections',
+      render: (text: number) => <span style={{ fontWeight: 500 
}}>{text}</span>,
+    },
+    {
+      title: t('common.actions'),
+      key: 'action',
+      render: () => (
+        <Space size="small">
+          <Button type="link" size="small" icon={<GearSix size={14} />}>
+            {t('brokerCluster.config')}
+          </Button>
+          <Button type="link" size="small" icon={<ArrowsClockwise size={14} 
/>}>
+            {t('brokerCluster.restart')}
+          </Button>
+        </Space>
+      ),
+    },
+  ];
+
+  const proxyColumns = [
+    {
+      title: t('brokerCluster.k8sCluster'),
+      dataIndex: 'k8sCluster',
+      key: 'k8sCluster',
+      render: (text: string) => <span style={{ fontWeight: 500 
}}>{text}</span>,
+    },
+    {
+      title: t('brokerCluster.proxyName'),
+      dataIndex: 'name',
+      key: 'name',
+      render: (text: string) => <span style={{ color: '#1677ff', fontWeight: 
500 }}>{text}</span>,
+    },
+    {
+      title: t('brokerCluster.status'),
+      dataIndex: 'status',
+      key: 'status',
+      render: renderStatus,
+    },
+    { title: t('brokerCluster.version'), dataIndex: 'version', key: 'version' 
},
+    {
+      title: t('brokerCluster.httpAddr'),
+      dataIndex: 'address',
+      key: 'address',
+      render: (text: string) => (
+        <code
+          style={{
+            fontSize: 12,
+            background: '#f5f5f5',
+            padding: '2px 6px',
+            borderRadius: 4,
+          }}
+        >
+          {text}
+        </code>
+      ),
+    },
+    {
+      title: t('brokerCluster.grpcAddr'),
+      dataIndex: 'grpcPort',
+      key: 'grpcPort',
+      render: (text: string) => (
+        <code
+          style={{
+            fontSize: 12,
+            background: '#f5f5f5',
+            padding: '2px 6px',
+            borderRadius: 4,
+          }}
+        >
+          {text}
+        </code>
+      ),
+    },
+    {
+      title: t('brokerCluster.connections'),
+      dataIndex: 'connections',
+      key: 'connections',
+      render: (text: number) => <span style={{ fontWeight: 500 
}}>{text}</span>,
+    },
+    {
+      title: t('common.actions'),
+      key: 'action',
+      render: () => (
+        <Space size="small">
+          <Button type="link" size="small" icon={<GearSix size={14} />}>
+            {t('brokerCluster.config')}
+          </Button>
+          <Button type="link" size="small" icon={<ArrowsClockwise size={14} 
/>}>
+            {t('brokerCluster.restart')}
+          </Button>
+        </Space>
+      ),
+    },
+  ];
+
+  return (
+    <div style={{ padding: 0 }}>
+      <div
+        style={{
+          display: 'flex',
+          justifyContent: 'space-between',
+          alignItems: 'center',
+          marginBottom: 20,
+        }}
+      >
+        <h2
+          style={{
+            fontSize: 20,
+            fontWeight: 600,
+            margin: 0,
+            display: 'flex',
+            alignItems: 'center',
+          }}
+        >
+          <Cloud size={22} style={{ marginRight: 8, color: '#1677ff' }} />
+          {t('brokerCluster.title')}
+        </h2>
+        <Space size="middle">
+          <Switch
+            checked={autoRefresh}
+            onChange={setAutoRefresh}
+            checkedChildren={t('common.liveRefresh')}
+            unCheckedChildren={t('brokerCluster.manual')}
+            size="small"
+          />
+          <Button icon={<ArrowClockwise size={14} />} size="small">
+            {t('common.reset')}
+          </Button>
+          <Button type="primary" icon={<Plus size={14} />}>
+            {t('brokerCluster.createCluster')}
+          </Button>
+        </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}
+                  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>
+    </div>
+  );
+};
+
+export default BrokerClusterPage;
diff --git a/web/src/pages/studio/__tests__/BrokerCluster.test.tsx 
b/web/src/pages/studio/__tests__/BrokerCluster.test.tsx
new file mode 100644
index 00000000..618014cc
--- /dev/null
+++ b/web/src/pages/studio/__tests__/BrokerCluster.test.tsx
@@ -0,0 +1,112 @@
+/*
+ * 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 { describe, it, expect, vi, beforeAll } from 'vitest';
+import { render, screen } from '@testing-library/react';
+import userEvent from '@testing-library/user-event';
+import { App } from 'antd';
+import { LangProvider } from '../../../i18n/LangContext';
+import BrokerCluster from '../BrokerCluster';
+
+// Mock matchMedia for antd responsive components
+beforeAll(() => {
+  Object.defineProperty(window, 'matchMedia', {
+    writable: true,
+    value: vi.fn().mockImplementation((query: string) => ({
+      matches: false,
+      media: query,
+      onchange: null,
+      addListener: vi.fn(),
+      removeListener: vi.fn(),
+      addEventListener: vi.fn(),
+      removeEventListener: vi.fn(),
+      dispatchEvent: vi.fn(),
+    })),
+  });
+});
+
+// Mock react-router-dom
+vi.mock('react-router-dom', () => ({
+  useNavigate: () => vi.fn(),
+  useParams: () => ({}),
+}));
+
+const renderWithProviders = (ui: React.ReactElement) => {
+  return render(
+    <App>
+      <LangProvider>{ui}</LangProvider>
+    </App>,
+  );
+};
+
+describe('BrokerCluster Page', () => {
+  it('should render the page title', () => {
+    renderWithProviders(<BrokerCluster />);
+    expect(screen.getByText('RocketMQ 集群')).toBeInTheDocument();
+  });
+
+  it('should render create cluster button', () => {
+    renderWithProviders(<BrokerCluster />);
+    expect(screen.getByText('新建集群')).toBeInTheDocument();
+  });
+
+  it('should render reset button', () => {
+    renderWithProviders(<BrokerCluster />);
+    expect(screen.getByText('重置')).toBeInTheDocument();
+  });
+
+  it('should display broker tab with mock data', () => {
+    renderWithProviders(<BrokerCluster />);
+    // Default tab is broker - check for mock broker data
+    expect(screen.getByText('broker-a')).toBeInTheDocument();
+    expect(screen.getByText('broker-b')).toBeInTheDocument();
+  });
+
+  it('should display broker status tags', () => {
+    renderWithProviders(<BrokerCluster />);
+    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 />);
+    const nsTab = screen.getByText('NameServer 管理');
+    await user.click(nsTab);
+    // After clicking, NameServer data should be visible
+    expect(screen.getByText('nameserver-a')).toBeInTheDocument();
+  });
+
+  it('should switch to Proxy tab on click', async () => {
+    const user = userEvent.setup();
+    renderWithProviders(<BrokerCluster />);
+    const proxyTab = screen.getByText('Proxy 管理');
+    await user.click(proxyTab);
+    // After clicking, Proxy data should be visible
+    expect(screen.getByText('proxy-a')).toBeInTheDocument();
+  });
+
+  it('should render config and restart action buttons', () => {
+    renderWithProviders(<BrokerCluster />);
+    const configButtons = screen.getAllByText('配置');
+    expect(configButtons.length).toBeGreaterThan(0);
+    const restartButtons = screen.getAllByText('重启');
+    expect(restartButtons.length).toBeGreaterThan(0);
+  });
+});

Reply via email to