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 bf05b536f fix(consumer): render unknown lags as unavailable (#2574)
bf05b536f is described below

commit bf05b536f688278a2c59d5c88b5c364f373f9707
Author: 0 <[email protected]>
AuthorDate: Thu Aug 27 15:32:48 2026 +0800

    fix(consumer): render unknown lags as unavailable (#2574)
    
    The backend reports -1 (ConsumerLagResolver.UNKNOWN) when a consumer lag
    cannot be determined, e.g. RocketMQ 5.0 gRPC consumers without proxy
    stats. The UI treated that sentinel like any other number: it rendered
    as a green low-backlog value in the group tables, counted it into the
    total backlog, and let an online group with an unknown lag look healthy
    and fully determined.
    
    - add a shared consumerLag helper (isLagAvailable/formatLag/lagSortValue)
    - GroupManagement and the consumer page now render unknown lags as a
      neutral "unavailable" label instead of a colored number
    - an unknown lag no longer triggers the backlog warning status or the
      backlog color, and the queue progress total is marked unavailable
      when any queue reports an unknown lag
    - unknown lags sort after every group with a known backlog in the
      lag-descending order and in the table column sorter
    
    Fixes #2500
    
    Signed-off-by: 123123213weqw <[email protected]>
---
 web/src/i18n/translations.ts                       |  2 +
 .../pages/instance/__tests__/ConsumerPage.test.tsx | 49 +++++++++++++++++
 web/src/pages/instance/consumer.tsx                | 62 +++++++++++++++++-----
 web/src/utils/consumerLag.test.ts                  | 45 ++++++++++++++++
 web/src/utils/consumerLag.ts                       | 33 ++++++++++++
 5 files changed, 179 insertions(+), 12 deletions(-)

diff --git a/web/src/i18n/translations.ts b/web/src/i18n/translations.ts
index 3ded5988a..d1ad6d4ce 100644
--- a/web/src/i18n/translations.ts
+++ b/web/src/i18n/translations.ts
@@ -1473,6 +1473,8 @@ const translations: Record<string, Record<Lang, string>> 
= {
   'brokerCluster.grpcAddr': { zh: 'gRPC 地址', en: 'gRPC Address' },
   'brokerCluster.connections': { zh: '连接数', en: 'Connections' },
 
+  'groupMgmt.lagUnavailable': { zh: '不可用', en: 'Unavailable' },
+
   // ─── SSL Settings ───
   'ssl.title': { zh: 'SSL/TLS 设置', en: 'SSL/TLS Settings' },
   'ssl.info': { zh: 'SSL/TLS 配置', en: 'SSL/TLS Configuration' },
diff --git a/web/src/pages/instance/__tests__/ConsumerPage.test.tsx 
b/web/src/pages/instance/__tests__/ConsumerPage.test.tsx
index da4cca616..9f3a2470b 100644
--- a/web/src/pages/instance/__tests__/ConsumerPage.test.tsx
+++ b/web/src/pages/instance/__tests__/ConsumerPage.test.tsx
@@ -911,4 +911,53 @@ describe('Consumer page', () => {
     });
     expect(await screen.findByText('消费组配置已保存')).toBeInTheDocument();
   });
+
+  it('renders an unknown (-1) lag as unavailable in the table and the lag 
detail', async () => {
+    const user = userEvent.setup();
+    vi.mocked(consumerService.listConsumerGroupPage).mockResolvedValue(
+      groupPage([
+        { ...group, name: 'unknown-lag-cg', totalLag: -1 },
+        { ...group, name: 'known-lag-cg', totalLag: 15000 },
+      ]),
+    );
+    renderWithProviders(<ConsumerPage />);
+
+    const unknownRow = await screen.findByRole('row', { name: /unknown-lag-cg/ 
});
+    expect(within(unknownRow).getByText('不可用')).toBeInTheDocument();
+    const knownRow = await screen.findByRole('row', { name: /\bknown-lag-cg\b/ 
});
+    expect(within(knownRow).queryByText('不可用')).not.toBeInTheDocument();
+
+    await user.click(within(unknownRow).getByRole('button', { name: /详\s*情/ 
}));
+    const dialog = await screen.findByRole('dialog', { name: /unknown-lag-cg/ 
});
+    expect(within(dialog).getByText('不可用')).toBeInTheDocument();
+  });
+
+  it('sorts groups with an unknown lag after known backlogs in lag order', 
async () => {
+    const user = userEvent.setup();
+    vi.mocked(consumerService.listConsumerGroupPage).mockResolvedValue(
+      groupPage([
+        { ...group, name: 'unknown-lag-cg', totalLag: -1 },
+        { ...group, name: 'known-lag-cg', totalLag: 15000 },
+      ]),
+    );
+    renderWithProviders(<ConsumerPage />);
+    await screen.findByRole('row', { name: /unknown-lag-cg/ });
+
+    await user.click(screen.getByText('名称升序'));
+    await user.click(await screen.findByText('堆积量降序'));
+    await waitFor(() => {
+      const rows = Array.from(document.querySelectorAll('tbody tr'));
+      const order = rows
+        .map((row) => row.textContent ?? '')
+        .map((text) =>
+          text.includes('unknown-lag-cg')
+            ? 'unknown'
+            : /\bknown-lag-cg\b/.test(text)
+              ? 'known'
+              : '',
+        )
+        .filter(Boolean);
+      expect(order).toEqual(['known', 'unknown']);
+    });
+  });
 });
diff --git a/web/src/pages/instance/consumer.tsx 
b/web/src/pages/instance/consumer.tsx
index 1da662bf5..588ed3523 100644
--- a/web/src/pages/instance/consumer.tsx
+++ b/web/src/pages/instance/consumer.tsx
@@ -97,13 +97,20 @@ import {
   type ResourceImportRow,
 } from '../../utils/resourceCsvImport';
 import { buildCsv, downloadCsv, type CsvColumn } from '../../utils/download';
+import { formatLag, isLagAvailable, lagSortValue } from 
'../../utils/consumerLag';
 import { tableScrollX } from '../../utils/table';
 
 const { Text } = Typography;
 
 /* ─── Helpers ─── */
 
+const UNKNOWN_LAG_COLOR = '#8c8c8c';
+const UNAVAILABLE_LAG_LABEL = '不可用';
+
 const lagColor = (lag: number): string => {
+  // The backend reports -1 when the lag cannot be determined; do not color it
+  // as healthy (green) or backlogged.
+  if (!isLagAvailable(lag)) return UNKNOWN_LAG_COLOR;
   if (lag >= 10_000) return '#ff4d4f';
   if (lag >= 1_000) return '#faad14';
   return '#52c41a';
@@ -164,7 +171,15 @@ const visibleConsumerGroups = (
   }
 
   if (sortKey === 'lag_desc') {
-    data = [...data].sort((left, right) => right.totalLag - left.totalLag);
+    // An unknown lag (-1) is not a measurable backlog, so it sorts after
+    // every group with a known backlog instead of first.
+    data = [...data].sort((left, right) => {
+      const leftKnown = isLagAvailable(left.totalLag);
+      const rightKnown = isLagAvailable(right.totalLag);
+      if (leftKnown !== rightKnown) return leftKnown ? -1 : 1;
+      if (!leftKnown) return 0;
+      return right.totalLag - left.totalLag;
+    });
   } else if (sortKey === 'name_asc') {
     data = [...data].sort((left, right) => 
left.name.localeCompare(right.name));
   }
@@ -512,7 +527,11 @@ const ConsumerPageContent = ({
       return (a.queueId ?? 0) - (b.queueId ?? 0);
     });
   }, [selectedProgress, progressTopic, progressTopicOptions]);
-  const visibleProgressLag = visibleProgress.reduce((sum, q) => sum + 
(q.diffTotal ?? 0), 0);
+  const hasUnknownProgressLag = visibleProgress.some((q) => 
!isLagAvailable(q.diffTotal));
+  const visibleProgressLag = visibleProgress.reduce(
+    (sum, q) => sum + (isLagAvailable(q.diffTotal) ? q.diffTotal : 0),
+    0,
+  );
 
   const openStackModal = async (consumerInstance: ConsumerInstance) => {
     if (!selectedGroup) return;
@@ -716,8 +735,13 @@ const ConsumerPageContent = ({
       key: 'totalLag',
       width: 96,
       align: 'right',
-      sorter: (a, b) => (a.totalLag ?? 0) - (b.totalLag ?? 0),
-      render: (lag: number) => (lag ?? 0).toLocaleString(),
+      sorter: (a, b) => lagSortValue(a.totalLag) - lagSortValue(b.totalLag),
+      render: (lag: number) =>
+        isLagAvailable(lag) ? (
+          lag.toLocaleString()
+        ) : (
+          <Text type="secondary">{UNAVAILABLE_LAG_LABEL}</Text>
+        ),
     },
     {
       title: '消费延迟',
@@ -1013,6 +1037,13 @@ const ConsumerPageContent = ({
       width: 120,
       align: 'right',
       render: (diff: number) => {
+        if (!isLagAvailable(diff)) {
+          return (
+            <Text type="secondary" style={{ fontWeight: 600 }}>
+              {UNAVAILABLE_LAG_LABEL}
+            </Text>
+          );
+        }
         const color = lagColor(diff);
         return (
           <Text style={{ color, fontWeight: 600, fontFamily: 'monospace' }}>
@@ -1287,6 +1318,7 @@ const ConsumerPageContent = ({
                           <Statistic
                             title="总堆积"
                             value={selectedGroup.totalLag}
+                            formatter={(value) => formatLag(Number(value), 
UNAVAILABLE_LAG_LABEL)}
                             prefix={
                               <ArrowsClockwise size={18} 
color={lagColor(selectedGroup.totalLag)} />
                             }
@@ -1515,14 +1547,20 @@ const ConsumerPageContent = ({
                         </Space>
                         <Space size={4}>
                           <Text type="secondary">总堆积:</Text>
-                          <Text
-                            strong
-                            style={{
-                              color: lagColor(visibleProgressLag),
-                            }}
-                          >
-                            {visibleProgressLag.toLocaleString()}
-                          </Text>
+                          {hasUnknownProgressLag ? (
+                            <Text strong style={{ color: UNKNOWN_LAG_COLOR }}>
+                              {UNAVAILABLE_LAG_LABEL}
+                            </Text>
+                          ) : (
+                            <Text
+                              strong
+                              style={{
+                                color: lagColor(visibleProgressLag),
+                              }}
+                            >
+                              {visibleProgressLag.toLocaleString()}
+                            </Text>
+                          )}
                         </Space>
                       </Space>
                     </Card>
diff --git a/web/src/utils/consumerLag.test.ts 
b/web/src/utils/consumerLag.test.ts
new file mode 100644
index 000000000..8d3da77a3
--- /dev/null
+++ b/web/src/utils/consumerLag.test.ts
@@ -0,0 +1,45 @@
+/*
+ * 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.
+ * 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, expect, it } from 'vitest';
+import { UNKNOWN_LAG, formatLag, isLagAvailable, lagSortValue } from 
'./consumerLag';
+
+describe('consumer lag helpers', () => {
+  it('treats the -1 sentinel and missing values as unavailable', () => {
+    expect(UNKNOWN_LAG).toBe(-1);
+    expect(isLagAvailable(UNKNOWN_LAG)).toBe(false);
+    expect(isLagAvailable(-5)).toBe(false);
+    expect(isLagAvailable(0)).toBe(true);
+    expect(isLagAvailable(1280)).toBe(true);
+    expect(isLagAvailable(undefined)).toBe(false);
+    expect(isLagAvailable(null)).toBe(false);
+    expect(isLagAvailable(Number.NaN)).toBe(false);
+  });
+
+  it('formats known lags numerically and unknown lags with the label', () => {
+    expect(formatLag(0, 'unavailable')).toBe('0');
+    expect(formatLag(10000, 'unavailable')).toBe(formatLag(10000));
+    expect(formatLag(UNKNOWN_LAG, 'unavailable')).toBe('unavailable');
+    expect(formatLag(undefined, 'unavailable')).toBe('unavailable');
+  });
+
+  it('sorts unknown lags after every known lag', () => {
+    expect(lagSortValue(0)).toBe(0);
+    expect(lagSortValue(999999999)).toBe(999999999);
+    expect(lagSortValue(UNKNOWN_LAG)).toBe(Number.MAX_SAFE_INTEGER);
+    expect(lagSortValue(null)).toBe(Number.MAX_SAFE_INTEGER);
+  });
+});
diff --git a/web/src/utils/consumerLag.ts b/web/src/utils/consumerLag.ts
new file mode 100644
index 000000000..ffb6e8392
--- /dev/null
+++ b/web/src/utils/consumerLag.ts
@@ -0,0 +1,33 @@
+/*
+ * 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.
+ * 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.
+ */
+
+/**
+ * Sentinel the backend (ConsumerLagResolver.UNKNOWN) reports when a consumer 
lag
+ * cannot be determined, e.g. RocketMQ 5.0 gRPC consumers without proxy stats.
+ */
+export const UNKNOWN_LAG = -1;
+
+export const isLagAvailable = (lag: number | null | undefined): lag is number 
=>
+  typeof lag === 'number' && Number.isFinite(lag) && lag >= 0;
+
+export const formatLag = (
+  lag: number | null | undefined,
+  unavailableLabel: string = String(UNKNOWN_LAG),
+): string => (isLagAvailable(lag) ? lag.toLocaleString() : unavailableLabel);
+
+/** Sort key that pushes unknown lags to the end of an ascending list. */
+export const lagSortValue = (lag: number | null | undefined): number =>
+  isLagAvailable(lag) ? lag : Number.MAX_SAFE_INTEGER;

Reply via email to