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 b77176c4 feat: add LiteTopic TTL status filtering (#761)
b77176c4 is described below

commit b77176c4d0c386da7b1757ed67d646030df39424
Author: yx9o <[email protected]>
AuthorDate: Mon Aug 3 11:37:28 2026 +0800

    feat: add LiteTopic TTL status filtering (#761)
---
 web/src/pages/studio/LiteTopic.tsx                | 27 +++++++++-
 web/src/pages/studio/__tests__/LiteTopic.test.tsx | 63 ++++++++++++++++++++---
 2 files changed, 82 insertions(+), 8 deletions(-)

diff --git a/web/src/pages/studio/LiteTopic.tsx 
b/web/src/pages/studio/LiteTopic.tsx
index 3aefb2d8..90f765b9 100644
--- a/web/src/pages/studio/LiteTopic.tsx
+++ b/web/src/pages/studio/LiteTopic.tsx
@@ -78,6 +78,8 @@ const getProgressStatus = (percent: number): 'exception' | 
'active' | 'normal' =
   return 'normal';
 };
 
+const knownTTLStatuses = new Set(['ACTIVE', 'EXPIRING_SOON', 'EXPIRED']);
+
 const collectNamespaces = (items: LiteTopicItem[]): string[] => {
   const namespaces = new Map<string, string>();
 
@@ -109,6 +111,7 @@ const LiteTopicPage: React.FC = () => {
   const [capabilitySupported, setCapabilitySupported] = useState(true);
   const [patternFilter, setPatternFilter] = useState('');
   const [namespaceFilter, setNamespaceFilter] = useState('');
+  const [ttlStatusFilter, setTTLStatusFilter] = useState('');
   const [namespaceOptions, setNamespaceOptions] = useState<string[]>([]);
 
   // Session drawer
@@ -288,6 +291,14 @@ const LiteTopicPage: React.FC = () => {
     return <Tag color={cfg.color}>{cfg.label}</Tag>;
   };
 
+  const filteredTopicList = topicList.filter((item) => {
+    if (!ttlStatusFilter) return true;
+    if (ttlStatusFilter === 'UNKNOWN') {
+      return !item.ttlStatus || !knownTTLStatuses.has(item.ttlStatus);
+    }
+    return item.ttlStatus === ttlStatusFilter;
+  });
+
   // ─── Columns ─────────────────────────────────────────────────
 
   const columns: ColumnsType<LiteTopicItem> = [
@@ -726,6 +737,20 @@ const LiteTopicPage: React.FC = () => {
               </Select.Option>
             ))}
           </Select>
+          <Select
+            aria-label={t('liteTopic.status')}
+            placeholder={t('liteTopic.status')}
+            value={ttlStatusFilter || undefined}
+            onChange={(value) => setTTLStatusFilter(value || '')}
+            style={{ width: 160 }}
+            allowClear
+            options={[
+              { value: 'ACTIVE', label: t('liteTopic.active') },
+              { value: 'EXPIRING_SOON', label: t('liteTopic.expiringSoon') },
+              { value: 'EXPIRED', label: t('liteTopic.expired') },
+              { value: 'UNKNOWN', label: t('liteTopic.unknown') },
+            ]}
+          />
           <Button type="primary" icon={<MagnifyingGlass size={14} />} 
onClick={handleSearch}>
             {t('common.search')}
           </Button>
@@ -736,7 +761,7 @@ const LiteTopicPage: React.FC = () => {
       <Card bordered={false} style={{ borderRadius: 8 }}>
         <Table
           columns={columns}
-          dataSource={topicList}
+          dataSource={filteredTopicList}
           rowKey={(record) => JSON.stringify([record.namespace, 
record.topicPattern])}
           loading={loading}
           pagination={{
diff --git a/web/src/pages/studio/__tests__/LiteTopic.test.tsx 
b/web/src/pages/studio/__tests__/LiteTopic.test.tsx
index 8680eddb..905692ec 100644
--- a/web/src/pages/studio/__tests__/LiteTopic.test.tsx
+++ b/web/src/pages/studio/__tests__/LiteTopic.test.tsx
@@ -80,9 +80,7 @@ const createQuota = (currentTopicCount: number): 
LiteTopicQuota => ({
 const selectNamespace = async (name: string) => {
   const user = userEvent.setup();
   await user.click(screen.getAllByRole('combobox')[0]);
-  await user.click(
-    await screen.findByText(name, { selector: 
'.ant-select-item-option-content' }),
-  );
+  await user.click(await screen.findByText(name, { selector: 
'.ant-select-item-option-content' }));
 };
 
 describe('LiteTopic Page', () => {
@@ -144,7 +142,9 @@ describe('LiteTopic Page', () => {
     expect(await screen.findByText('filtered-*')).toBeInTheDocument();
 
     await user.click(screen.getAllByRole('combobox')[0]);
-    const dropdown = 
document.querySelector('.ant-select-dropdown:not(.ant-select-dropdown-hidden)');
+    const dropdown = document.querySelector(
+      '.ant-select-dropdown:not(.ant-select-dropdown-hidden)',
+    );
     expect(dropdown).not.toBeNull();
     expect(
       
Array.from(dropdown!.querySelectorAll('.ant-select-item-option-content')).map(
@@ -153,6 +153,49 @@ describe('LiteTopic Page', () => {
     ).toEqual(['全部命名空间', 'Alpha', 'zeta']);
   });
 
+  it('filters the current results by TTL status without requesting the list 
again', async () => {
+    apiMocks.queryLiteTopicList.mockResolvedValue([
+      { namespace: 'default', topicPattern: 'active-*', ttlStatus: 'ACTIVE' },
+      { namespace: 'default', topicPattern: 'expiring-*', ttlStatus: 
'EXPIRING_SOON' },
+      { namespace: 'default', topicPattern: 'expired-*', ttlStatus: 'EXPIRED' 
},
+      { namespace: 'default', topicPattern: 'unknown-*', ttlStatus: 'UNKNOWN' 
},
+      { namespace: 'default', topicPattern: 'missing-status-*' },
+    ]);
+
+    const user = userEvent.setup();
+    renderPage();
+
+    expect(await screen.findByText('active-*')).toBeInTheDocument();
+    expect(screen.getByText('expiring-*')).toBeInTheDocument();
+    expect(screen.getByText('expired-*')).toBeInTheDocument();
+    expect(screen.getByText('unknown-*')).toBeInTheDocument();
+    expect(screen.getByText('missing-status-*')).toBeInTheDocument();
+
+    const initialListRequestCount = 
apiMocks.queryLiteTopicList.mock.calls.length;
+    await user.click(screen.getByRole('combobox', { name: '状态' }));
+    await user.click(
+      await screen.findByText('即将过期', { selector: 
'.ant-select-item-option-content' }),
+    );
+
+    expect(screen.queryByText('active-*')).not.toBeInTheDocument();
+    expect(screen.getByText('expiring-*')).toBeInTheDocument();
+    expect(screen.queryByText('expired-*')).not.toBeInTheDocument();
+    expect(screen.queryByText('unknown-*')).not.toBeInTheDocument();
+    expect(screen.queryByText('missing-status-*')).not.toBeInTheDocument();
+
+    await user.click(screen.getByRole('combobox', { name: '状态' }));
+    await user.click(
+      await screen.findByText('未知', { selector: 
'.ant-select-item-option-content' }),
+    );
+
+    expect(screen.queryByText('active-*')).not.toBeInTheDocument();
+    expect(screen.queryByText('expiring-*')).not.toBeInTheDocument();
+    expect(screen.queryByText('expired-*')).not.toBeInTheDocument();
+    expect(screen.getByText('unknown-*')).toBeInTheDocument();
+    expect(screen.getByText('missing-status-*')).toBeInTheDocument();
+    
expect(apiMocks.queryLiteTopicList).toHaveBeenCalledTimes(initialListRequestCount);
+  });
+
   it('keeps an early filtered display while a delayed bootstrap supplies 
namespace options', async () => {
     const capability = createDeferred<{ supported: boolean }>();
     const bootstrapList = createDeferred<LiteTopicItem[]>();
@@ -194,7 +237,9 @@ describe('LiteTopic Page', () => {
     expect(apiMocks.queryLiteTopicQuota).toHaveBeenCalledTimes(1);
 
     await user.click(screen.getAllByRole('combobox')[0]);
-    const dropdown = 
document.querySelector('.ant-select-dropdown:not(.ant-select-dropdown-hidden)');
+    const dropdown = document.querySelector(
+      '.ant-select-dropdown:not(.ant-select-dropdown-hidden)',
+    );
     expect(dropdown).not.toBeNull();
     expect(
       
Array.from(dropdown!.querySelectorAll('.ant-select-item-option-content')).map(
@@ -241,7 +286,9 @@ describe('LiteTopic Page', () => {
     expect(screen.queryByText('90 / 100')).not.toBeInTheDocument();
 
     await user.click(screen.getAllByRole('combobox')[0]);
-    const dropdown = 
document.querySelector('.ant-select-dropdown:not(.ant-select-dropdown-hidden)');
+    const dropdown = document.querySelector(
+      '.ant-select-dropdown:not(.ant-select-dropdown-hidden)',
+    );
     expect(dropdown).not.toBeNull();
     expect(
       
Array.from(dropdown!.querySelectorAll('.ant-select-item-option-content')).map(
@@ -269,7 +316,9 @@ describe('LiteTopic Page', () => {
     await act(async () => {
       capability.resolve({ supported: false });
     });
-    expect(await screen.findByText('当前集群不支持 LiteTopic,请升级到 RocketMQ 
5.x')).toBeInTheDocument();
+    expect(
+      await screen.findByText('当前集群不支持 LiteTopic,请升级到 RocketMQ 5.x'),
+    ).toBeInTheDocument();
 
     await act(async () => {
       earlyQuota.resolve(createQuota(20));

Reply via email to