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));