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;