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 0a00548d feat: add subscription consistency diagnostics (#724)
0a00548d is described below

commit 0a00548d4223fd95b97aa165b17660a70fe7e367
Author: yx9o <[email protected]>
AuthorDate: Mon Aug 3 11:14:13 2026 +0800

    feat: add subscription consistency diagnostics (#724)
---
 .../pages/instance/__tests__/ConsumerPage.test.tsx |  86 +++++++++++++++
 web/src/pages/instance/consumer.tsx                | 116 +++++++++++++++++++--
 2 files changed, 193 insertions(+), 9 deletions(-)

diff --git a/web/src/pages/instance/__tests__/ConsumerPage.test.tsx 
b/web/src/pages/instance/__tests__/ConsumerPage.test.tsx
index 9d413f97..8f5b6d7c 100644
--- a/web/src/pages/instance/__tests__/ConsumerPage.test.tsx
+++ b/web/src/pages/instance/__tests__/ConsumerPage.test.tsx
@@ -78,6 +78,7 @@ const renderWithProviders = (ui: React.ReactElement) =>
 
 describe('Consumer page', () => {
   beforeEach(() => {
+    vi.clearAllMocks();
     vi.mocked(consumerService.listConsumerGroups).mockResolvedValue([group]);
     vi.mocked(consumerService.getConsumerProgress).mockResolvedValue([
       {
@@ -122,4 +123,89 @@ describe('Consumer page', () => {
     expect(consumerService.getConsumerGroup).not.toHaveBeenCalled();
     await waitFor(() => 
expect(screen.getAllByText('remote-topic').length).toBeGreaterThan(0));
   });
+
+  it('highlights inconsistent subscriptions and refreshes the check result', 
async () => {
+    vi.mocked(consumerService.getConsumerSubscriptions)
+      .mockResolvedValueOnce([
+        {
+          topic: 'remote-topic',
+          expression: '*',
+          type: 'NORMAL',
+          filterMode: '全量',
+          consistency: 'consistent',
+        },
+        {
+          topic: 'stale-topic',
+          expression: 'important',
+          type: 'NORMAL',
+          filterMode: 'Tag 过滤',
+          consistency: 'inconsistent',
+        },
+      ])
+      .mockResolvedValueOnce([
+        {
+          topic: 'remote-topic',
+          expression: '*',
+          type: 'NORMAL',
+          filterMode: '全量',
+          consistency: 'consistent',
+        },
+        {
+          topic: 'stale-topic',
+          expression: 'important',
+          type: 'NORMAL',
+          filterMode: 'Tag 过滤',
+          consistency: 'consistent',
+        },
+      ]);
+
+    const user = userEvent.setup();
+    renderWithProviders(<ConsumerPage />);
+
+    await user.click(await screen.findByRole('button', { name: /详情/ }));
+
+    expect(await screen.findByText('发现 1 个订阅配置不一致')).toBeInTheDocument();
+    
expect(screen.getByText('consistent').closest('.ant-tag')).toHaveClass('ant-tag-green');
+    
expect(screen.getByText('inconsistent').closest('.ant-tag')).toHaveClass('ant-tag-orange');
+    await user.click(screen.getByRole('checkbox', { name: '仅看不一致' }));
+    expect(screen.getByText('stale-topic')).toBeInTheDocument();
+
+    await user.click(screen.getByRole('button', { name: '重新检查' }));
+
+    expect(await screen.findByText('全部 2 个订阅配置一致')).toBeInTheDocument();
+    expect(consumerService.getConsumerSubscriptions).toHaveBeenCalledTimes(2);
+  });
+
+  it('keeps unknown consistency values separate from mismatches', async () => {
+    vi.mocked(consumerService.getConsumerSubscriptions).mockResolvedValue([
+      {
+        topic: 'unknown-topic',
+        expression: '*',
+        type: 'NORMAL',
+        filterMode: '全量',
+        consistency: 'UNKNOWN',
+      },
+    ]);
+
+    const user = userEvent.setup();
+    renderWithProviders(<ConsumerPage />);
+
+    await user.click(await screen.findByRole('button', { name: /详情/ }));
+
+    expect(await screen.findByText('1 个订阅配置状态未知')).toBeInTheDocument();
+    expect(screen.getByRole('checkbox', { name: '仅看不一致' })).toBeDisabled();
+  });
+
+  it('reports a failed consistency check without presenting stale data as 
current', async () => {
+    vi.mocked(consumerService.getConsumerSubscriptions).mockRejectedValue(
+      new Error('request failed'),
+    );
+
+    const user = userEvent.setup();
+    renderWithProviders(<ConsumerPage />);
+
+    await user.click(await screen.findByRole('button', { name: /详情/ }));
+
+    expect(await 
screen.findByText('订阅一致性检查失败,当前保留上次检查结果')).toBeInTheDocument();
+  });
 });
diff --git a/web/src/pages/instance/consumer.tsx 
b/web/src/pages/instance/consumer.tsx
index ccf0f46d..f21abb00 100644
--- a/web/src/pages/instance/consumer.tsx
+++ b/web/src/pages/instance/consumer.tsx
@@ -17,9 +17,11 @@
 
 import { useCallback, useEffect, useMemo, useState } from 'react';
 import {
+  Alert,
   Table,
   Card,
   Button,
+  Checkbox,
   Tag,
   Space,
   Input,
@@ -115,6 +117,20 @@ const formatDateTime = (dateStr: string): string => {
   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 =>
+  ['consistent', '一致'].includes(normalizedConsistency(value));
+
+const isInconsistentValue = (value?: string | null): boolean =>
+  ['inconsistent', '不一致'].includes(normalizedConsistency(value));
+
+const isConsistentSubscription = (subscription: SubscriptionEntry): boolean =>
+  isConsistentValue(subscription.consistency);
+
+const isInconsistentSubscription = (subscription: SubscriptionEntry): boolean 
=>
+  isInconsistentValue(subscription.consistency);
+
 /* ═══════════════════════════════════════════
    ConsumerPage
    ═══════════════════════════════════════════ */
@@ -139,6 +155,13 @@ const ConsumerPage = () => {
   const [subscriptionsByGroup, setSubscriptionsByGroup] = useState<
     Record<string, SubscriptionEntry[]>
   >({});
+  const [subscriptionLoadingByGroup, setSubscriptionLoadingByGroup] = useState<
+    Record<string, boolean>
+  >({});
+  const [subscriptionErrorByGroup, setSubscriptionErrorByGroup] = 
useState<Record<string, boolean>>(
+    {},
+  );
+  const [showOnlyInconsistent, setShowOnlyInconsistent] = useState(false);
   const [progressByGroup, setProgressByGroup] = useState<Record<string, 
QueueProgress[]>>({});
 
   useEffect(() => {
@@ -162,13 +185,18 @@ const ConsumerPage = () => {
   }, [t]);
 
   const loadSubscriptions = useCallback(
-    async (groupName: string) => {
-      if (subscriptionsByGroup[groupName]) return;
+    async (groupName: string, force = false) => {
+      if (!force && subscriptionsByGroup[groupName]) return;
+      setSubscriptionLoadingByGroup((prev) => ({ ...prev, [groupName]: true 
}));
+      setSubscriptionErrorByGroup((prev) => ({ ...prev, [groupName]: false }));
       try {
         const subscriptions = await getConsumerSubscriptions(groupName);
         setSubscriptionsByGroup((prev) => ({ ...prev, [groupName]: 
subscriptions }));
       } catch {
+        setSubscriptionErrorByGroup((prev) => ({ ...prev, [groupName]: true 
}));
         message.error(t('consumer.fetchSubscriptionsFailed', { name: groupName 
}));
+      } finally {
+        setSubscriptionLoadingByGroup((prev) => ({ ...prev, [groupName]: false 
}));
       }
     },
     [subscriptionsByGroup, t],
@@ -212,6 +240,7 @@ const ConsumerPage = () => {
   /* ─── Open detail modal ─── */
   const openModal = (group: ConsumerGroup) => {
     setSelectedGroup(group);
+    setShowOnlyInconsistent(false);
     setModalOpen(true);
     void loadSubscriptions(group.name);
     void loadProgress(group.name);
@@ -220,6 +249,14 @@ const ConsumerPage = () => {
   const selectedSubscriptions = selectedGroup
     ? (subscriptionsByGroup[selectedGroup.name] ?? [])
     : [];
+  const inconsistentSubscriptions = 
selectedSubscriptions.filter(isInconsistentSubscription);
+  const unknownSubscriptions = selectedSubscriptions.filter(
+    (subscription) =>
+      !isConsistentSubscription(subscription) && 
!isInconsistentSubscription(subscription),
+  );
+  const visibleSubscriptions = showOnlyInconsistent
+    ? inconsistentSubscriptions
+    : selectedSubscriptions;
   const selectedProgress = selectedGroup ? 
(progressByGroup[selectedGroup.name] ?? []) : [];
 
   /* ═══════════════════════════════════════════
@@ -383,7 +420,15 @@ const ConsumerPage = () => {
       dataIndex: 'consistency',
       key: 'consistency',
       width: 110,
-      render: (v: string) => <Tag color={v === '一致' ? 'green' : 
'orange'}>{v}</Tag>,
+      render: (value: string) => (
+        <Tag
+          color={
+            isConsistentValue(value) ? 'green' : isInconsistentValue(value) ? 
'orange' : 'default'
+          }
+        >
+          {value}
+        </Tag>
+      ),
     },
     {
       title: '订阅模式',
@@ -649,6 +694,7 @@ const ConsumerPage = () => {
                   columns={subscriptionSubColumns}
                   dataSource={subscriptionsByGroup[record.name] ?? []}
                   rowKey="topic"
+                  loading={subscriptionLoadingByGroup[record.name]}
                   pagination={false}
                   size="small"
                 />
@@ -679,6 +725,7 @@ const ConsumerPage = () => {
         onCancel={() => {
           setModalOpen(false);
           setSelectedGroup(null);
+          setShowOnlyInconsistent(false);
         }}
         width={800}
         destroyOnClose
@@ -819,16 +866,67 @@ const ConsumerPage = () => {
 
                     {/* 订阅关系 */}
                     <div style={{ marginTop: 24 }}>
-                      <Flex align="center" gap={6} style={{ marginBottom: 12 
}}>
-                        <ListBullets size={15} color="#1677ff" />
-                        <Text strong style={{ fontSize: 14 }}>
-                          订阅关系
-                        </Text>
+                      <Flex justify="space-between" align="center" style={{ 
marginBottom: 12 }}>
+                        <Flex align="center" gap={6}>
+                          <ListBullets size={15} color="#1677ff" />
+                          <Text strong style={{ fontSize: 14 }}>
+                            订阅一致性检查
+                          </Text>
+                        </Flex>
+                        <Button
+                          size="small"
+                          icon={<ArrowsClockwise size={14} />}
+                          
loading={subscriptionLoadingByGroup[selectedGroup.name]}
+                          onClick={() => {
+                            setShowOnlyInconsistent(false);
+                            void loadSubscriptions(selectedGroup.name, true);
+                          }}
+                        >
+                          重新检查
+                        </Button>
                       </Flex>
+                      <Alert
+                        showIcon
+                        type={
+                          subscriptionErrorByGroup[selectedGroup.name]
+                            ? 'error'
+                            : inconsistentSubscriptions.length > 0 ||
+                                unknownSubscriptions.length > 0
+                              ? 'warning'
+                              : selectedSubscriptions.length > 0
+                                ? 'success'
+                                : 'info'
+                        }
+                        message={
+                          subscriptionErrorByGroup[selectedGroup.name]
+                            ? '订阅一致性检查失败,当前保留上次检查结果'
+                            : subscriptionLoadingByGroup[selectedGroup.name] &&
+                                selectedSubscriptions.length === 0
+                              ? '正在检查订阅一致性'
+                              : inconsistentSubscriptions.length > 0
+                                ? `发现 ${inconsistentSubscriptions.length} 
个订阅配置不一致`
+                                : unknownSubscriptions.length > 0
+                                  ? `${unknownSubscriptions.length} 个订阅配置状态未知`
+                                  : selectedSubscriptions.length > 0
+                                    ? `全部 ${selectedSubscriptions.length} 
个订阅配置一致`
+                                    : '暂无订阅关系可检查'
+                        }
+                        action={
+                          <Checkbox
+                            checked={showOnlyInconsistent}
+                            disabled={inconsistentSubscriptions.length === 0}
+                            onChange={(event) => 
setShowOnlyInconsistent(event.target.checked)}
+                          >
+                            仅看不一致
+                          </Checkbox>
+                        }
+                        style={{ marginBottom: 12 }}
+                      />
                       <Table
                         columns={subscriptionSubColumns}
-                        dataSource={selectedSubscriptions}
+                        dataSource={visibleSubscriptions}
                         rowKey="topic"
+                        
loading={subscriptionLoadingByGroup[selectedGroup.name]}
                         pagination={false}
                         size="small"
                       />

Reply via email to