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 <[email protected]>
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.