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.

Reply via email to