You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@rocketmq.apache.org by li...@apache.org on 2026/10/15 10:35:09 UTC
(rocketmq-dashboard) branch rocketmq-studio updated: fix(litetopic): keep an unmeasured TTL and last-active time out of the first sort position (#6539)
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 69e5cd4dd fix(litetopic): keep an unmeasured TTL and last-active time out of the first sort position (#6539)
69e5cd4dd is described below
commit 69e5cd4dd09bae3d0ee330bb16a310dbef913cbc
Author: Apulupie <16...@users.noreply.github.com>
AuthorDate: Thu Oct 15 18:35:03 2026 +0800
fix(litetopic): keep an unmeasured TTL and last-active time out of the first sort position (#6539)
Two sortable columns of the Lite topic table rank a pattern the provider could not measure as if
it were the smallest value: the average TTL as a zero TTL and the last-active instant as epoch 0,
while the cells render `-` for the same rows (`LiteTopic.tsx` `formatDuration` / `formatTime`).
The provider leaves both fields unset: `RocketMQLiteTopicProvider.buildSummary` only sets
`lastActiveTime` after a session reported an access (a pattern with no connected consumer therefore
carries none), and `toMillis` returns null when the broker has no TTL attribute, which is exactly
the case whose TTL status the page already classifies as unknown. Sorting ascending by either column
put those patterns first, so a reader looking for the shortest TTL or the longest idle time saw
unmeasured rows claim those positions.
Both sorters now use a sort-order-aware key that keeps an unset value last in both directions, the
same technique `BrokerCluster.tsx` uses for an unmeasured TPS: Ant Design negates the comparator for
a descending column, so an unset value cannot simply sort as the lowest number. The other three
sortable columns are untouched because their fields are always set (`topicCount` is floored at 0,
`consumerCount` and `totalBacklog` are counters).
---
web/src/pages/studio/LiteTopic.tsx | 19 ++++++++--
web/src/pages/studio/__tests__/LiteTopic.test.tsx | 44 +++++++++++++++++++++++
2 files changed, 61 insertions(+), 2 deletions(-)
diff --git a/web/src/pages/studio/LiteTopic.tsx b/web/src/pages/studio/LiteTopic.tsx
index 1fa4a5030..3d810a882 100644
--- a/web/src/pages/studio/LiteTopic.tsx
+++ b/web/src/pages/studio/LiteTopic.tsx
@@ -76,6 +76,18 @@ export const formatTime = (timestamp: number | undefined | null): string => {
return Number.isNaN(date.getTime()) ? '-' : date.toLocaleString();
};
+/**
+ * Sort key for a column value the provider can leave unset. The cells render `-` in that case
+ * (`formatDuration` / `formatTime`), and neither an absent TTL nor an absent last-active instant
+ * may sort as a measured zero: ascending, that would place the pattern before every measurable one
+ * as if it had expired first or been idle longest. Ant Design negates the comparator for a
+ * descending column, so the unset value has to stay last in both directions.
+ */
+const unmeasuredSortKey = (
+ value: number | undefined,
+ sortOrder?: 'ascend' | 'descend' | null,
+): number => value ?? (sortOrder === 'descend' ? Number.MIN_SAFE_INTEGER : Number.MAX_SAFE_INTEGER);
+
const getProgressStatus = (percent: number): 'exception' | 'active' | 'normal' => {
if (percent >= 90) return 'exception';
if (percent >= 70) return 'active';
@@ -466,7 +478,8 @@ const LiteTopicPage: React.FC = () => {
dataIndex: 'averageTTL',
key: 'averageTTL',
render: (val: number) => formatDuration(val),
- sorter: (a, b) => (a.averageTTL || 0) - (b.averageTTL || 0),
+ sorter: (a, b, sortOrder?: 'ascend' | 'descend' | null) =>
+ unmeasuredSortKey(a.averageTTL, sortOrder) - unmeasuredSortKey(b.averageTTL, sortOrder),
},
{
title: t('liteTopic.status'),
@@ -479,7 +492,9 @@ const LiteTopicPage: React.FC = () => {
dataIndex: 'lastActiveTime',
key: 'lastActiveTime',
render: (val: number) => formatTime(val),
- sorter: (a, b) => (a.lastActiveTime || 0) - (b.lastActiveTime || 0),
+ sorter: (a, b, sortOrder?: 'ascend' | 'descend' | null) =>
+ unmeasuredSortKey(a.lastActiveTime, sortOrder) -
+ unmeasuredSortKey(b.lastActiveTime, sortOrder),
},
{
title: t('liteTopic.actions'),
diff --git a/web/src/pages/studio/__tests__/LiteTopic.test.tsx b/web/src/pages/studio/__tests__/LiteTopic.test.tsx
index 693723a5b..46622e325 100644
--- a/web/src/pages/studio/__tests__/LiteTopic.test.tsx
+++ b/web/src/pages/studio/__tests__/LiteTopic.test.tsx
@@ -252,6 +252,50 @@ describe('LiteTopic Page', () => {
localStorage.removeItem('rocketmq-studio-language');
}
});
+
+ it('sorts a pattern whose TTL and last-active value are unset after the measurable ones', async () => {
+ apiMocks.queryLiteTopicList.mockResolvedValue([
+ {
+ namespace: 'default',
+ topicPattern: 'aaa-unmeasured-*',
+ // The provider leaves these unset when the broker reports no TTL attribute and no session
+ // ever reported an access; both cells render '-'.
+ sessionIds: [],
+ },
+ {
+ namespace: 'default',
+ topicPattern: 'zzz-measured-*',
+ averageTTL: 3_600_000,
+ lastActiveTime: Date.UTC(2026, 7, 20, 10, 0, 0),
+ sessionIds: ['session-1'],
+ },
+ ] as LiteTopicItem[]);
+ const user = userEvent.setup();
+ renderPage();
+
+ await screen.findByText('zzz-measured-*');
+ const rowOrder = () => screen.getAllByRole('row').map((row) => row.textContent ?? '');
+ const indexOf = (pattern: string) => rowOrder().findIndex((text) => text.includes(pattern));
+
+ // Ascending: a value the column renders as '-' must not sort as a zero TTL, which would
+ // place that pattern as if it had expired before every measurable one.
+ await user.click(screen.getByText('平均 TTL'));
+ await waitFor(() =>
+ expect(indexOf('aaa-unmeasured-*')).toBeGreaterThan(indexOf('zzz-measured-*')),
+ );
+
+ // The unset value stays last in the descending direction too instead of swapping to the top.
+ await user.click(screen.getByText('平均 TTL'));
+ await waitFor(() =>
+ expect(indexOf('aaa-unmeasured-*')).toBeGreaterThan(indexOf('zzz-measured-*')),
+ );
+
+ await user.click(screen.getByText('最后活跃'));
+ await waitFor(() =>
+ expect(indexOf('aaa-unmeasured-*')).toBeGreaterThan(indexOf('zzz-measured-*')),
+ );
+ });
+
it('sends the owning instance id when extending TTL', async () => {
// LiteTopicTTLUpdateDTO requires a non-blank instanceId; only the
// Apache-vendor instance is a candidate for the ownership-checked write.