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 c59de545f fix(web): reset pagination when filters change (#2434)
c59de545f is described below
commit c59de545f743f60ba76e29e119faf0164ab38c45
Author: btlqql <[email protected]>
AuthorDate: Fri Aug 21 18:03:53 2026 +0800
fix(web): reset pagination when filters change (#2434)
---
.../pages/cluster/__tests__/ClientsPage.test.tsx | 19 +++++++++++
web/src/pages/cluster/clients.tsx | 34 +++++++++++++++++---
web/src/pages/studio/GroupManagement.tsx | 17 ++++++++--
web/src/pages/studio/LiteTopic.tsx | 19 +++++++++--
.../studio/__tests__/GroupManagement.test.tsx | 37 ++++++++++++++++++++++
web/src/pages/studio/__tests__/LiteTopic.test.tsx | 24 ++++++++++++++
6 files changed, 141 insertions(+), 9 deletions(-)
diff --git a/web/src/pages/cluster/__tests__/ClientsPage.test.tsx
b/web/src/pages/cluster/__tests__/ClientsPage.test.tsx
index b6b06a274..8573facb5 100644
--- a/web/src/pages/cluster/__tests__/ClientsPage.test.tsx
+++ b/web/src/pages/cluster/__tests__/ClientsPage.test.tsx
@@ -140,6 +140,25 @@ const renderWithProviders = (ui: React.ReactElement) =>
);
describe('Clients page', () => {
+ it('returns to the first page when the connection search changes', async ()
=> {
+ const pagedConnections = Array.from({ length: 21 }, (_, index) => ({
+ ...connection,
+ clientId: `client-${String(index).padStart(2, '0')}`,
+ address: `10.0.1.${index + 1}:49152`,
+ }));
+
vi.mocked(connectionsService.listConnections).mockResolvedValue(pagedConnections);
+ const user = userEvent.setup();
+ const { container } = renderWithProviders(<ClientsPage />);
+
+ await screen.findByText('client-00');
+ await user.click(container.querySelector('.ant-pagination-next button')!);
+ expect(await screen.findByText('client-20')).toBeInTheDocument();
+ expect(screen.queryByText('client-00')).not.toBeInTheDocument();
+
+ await user.type(screen.getByPlaceholderText('搜索 Client ID 或地址'),
'client-00');
+ expect(await screen.findByText('client-00')).toBeInTheDocument();
+ });
+
it('loads connections for the first online broker cluster', async () => {
renderWithProviders(<ClientsPage />);
diff --git a/web/src/pages/cluster/clients.tsx
b/web/src/pages/cluster/clients.tsx
index 349b5265f..24700fcef 100644
--- a/web/src/pages/cluster/clients.tsx
+++ b/web/src/pages/cluster/clients.tsx
@@ -135,6 +135,8 @@ const ClientsPage = () => {
const [registryLoadKey, setRegistryLoadKey] = useState(0);
const [connectionLoadKey, setConnectionLoadKey] = useState(0);
const [columnFilters, setColumnFilters] = useState<ClientTableFilters>({});
+ const [currentPage, setCurrentPage] = useState(1);
+ const [pageSize, setPageSize] = useState(20);
const selectedCluster = registryClusters.find((cluster) => cluster.endpoint
=== selectedEndpoint);
@@ -148,6 +150,7 @@ const ClientsPage = () => {
);
const handleNameserverChange = (endpoint: string) => {
+ setCurrentPage(1);
setSelectedEndpoint(endpoint);
setConnections([]);
setClusterFilter('ALL');
@@ -282,6 +285,9 @@ const ClientsPage = () => {
);
}, [columnFilters, filtered]);
+ const lastPage = Math.max(1, Math.ceil(exportConnections.length / pageSize));
+ const clampedCurrentPage = Math.min(currentPage, lastPage);
+
const handleExport = () => {
const filename = `rocketmq-client-connections-${new
Date().toISOString().slice(0, 10)}.csv`;
const csv = buildCsv(CLIENT_CONNECTION_EXPORT_COLUMNS, exportConnections);
@@ -479,7 +485,10 @@ const ClientsPage = () => {
<Select
aria-label={t('clients.cluster')}
value={clusterFilter}
- onChange={setClusterFilter}
+ onChange={(value) => {
+ setClusterFilter(value);
+ setCurrentPage(1);
+ }}
style={{ width: 180 }}
options={clusterOptions}
/>
@@ -487,8 +496,14 @@ const ClientsPage = () => {
placeholder={t('clients.searchPlaceholder')}
allowClear
value={search}
- onChange={(e) => setSearch(e.target.value)}
- onSearch={setSearch}
+ onChange={(e) => {
+ setSearch(e.target.value);
+ setCurrentPage(1);
+ }}
+ onSearch={(value) => {
+ setSearch(value);
+ setCurrentPage(1);
+ }}
style={{ width: 280 }}
prefix={<MagnifyingGlass size={14} color="#9CA3AF" />}
/>
@@ -572,10 +587,19 @@ const ClientsPage = () => {
`${connection.type}:${connection.clientId}:${connection.groupOrTopic}`
}
loading={loading}
- onChange={(_, filters) => setColumnFilters(filters)}
+ onChange={(pagination, filters, _sorter, extra) => {
+ setColumnFilters(filters);
+ if (extra.action === 'filter') {
+ setCurrentPage(1);
+ return;
+ }
+ setCurrentPage(pagination.current ?? 1);
+ setPageSize(pagination.pageSize ?? 20);
+ }}
scroll={{ x: tableScrollX(columns) }}
pagination={{
- pageSize: 20,
+ current: clampedCurrentPage,
+ pageSize,
showSizeChanger: true,
showTotal: (total) => `${t('common.total')} ${total}`,
}}
diff --git a/web/src/pages/studio/GroupManagement.tsx
b/web/src/pages/studio/GroupManagement.tsx
index 29038d6c4..e4125f581 100644
--- a/web/src/pages/studio/GroupManagement.tsx
+++ b/web/src/pages/studio/GroupManagement.tsx
@@ -71,6 +71,8 @@ const GroupManagementPage = () => {
const [progressLoading, setProgressLoading] = useState(false);
const [subscriptionError, setSubscriptionError] = useState<string |
null>(null);
const [progressError, setProgressError] = useState<string | null>(null);
+ const [currentPage, setCurrentPage] = useState(1);
+ const [pageSize, setPageSize] = useState(10);
const listRequestId = useRef(0);
const listInFlight = useRef<Promise<void> | null>(null);
const listRefreshQueued = useRef(false);
@@ -188,6 +190,9 @@ const GroupManagementPage = () => {
[groups, normalizedSearchText],
);
+ const lastPage = Math.max(1, Math.ceil(filteredGroupData.length / pageSize));
+ const clampedCurrentPage = Math.min(currentPage, lastPage);
+
const columns = [
{
title: t('groupMgmt.groupName'),
@@ -317,7 +322,10 @@ const GroupManagementPage = () => {
placeholder={t('groupMgmt.searchPlaceholder')}
prefix={<MagnifyingGlass size={14} />}
value={searchText}
- onChange={(e) => setSearchText(e.target.value)}
+ onChange={(e) => {
+ setSearchText(e.target.value);
+ setCurrentPage(1);
+ }}
style={{ width: 240 }}
allowClear
/>
@@ -346,7 +354,12 @@ const GroupManagementPage = () => {
}
loading={loading}
pagination={{
- pageSize: 10,
+ current: clampedCurrentPage,
+ pageSize,
+ onChange: (page, nextPageSize) => {
+ setCurrentPage(page);
+ setPageSize(nextPageSize);
+ },
showTotal: (total) => `${t('common.total')} ${total} Group`,
showSizeChanger: true,
}}
diff --git a/web/src/pages/studio/LiteTopic.tsx
b/web/src/pages/studio/LiteTopic.tsx
index 7692ad95d..4365045b7 100644
--- a/web/src/pages/studio/LiteTopic.tsx
+++ b/web/src/pages/studio/LiteTopic.tsx
@@ -113,6 +113,8 @@ const LiteTopicPage: React.FC = () => {
const [namespaceFilter, setNamespaceFilter] = useState('');
const [ttlStatusFilter, setTTLStatusFilter] = useState('');
const [namespaceOptions, setNamespaceOptions] = useState<string[]>([]);
+ const [currentPage, setCurrentPage] = useState(1);
+ const [pageSize, setPageSize] = useState(10);
// Session drawer
const [sessionDrawerOpen, setSessionDrawerOpen] = useState(false);
@@ -240,6 +242,7 @@ const LiteTopicPage: React.FC = () => {
}, [fetchData]);
const handleSearch = () => {
+ setCurrentPage(1);
void fetchData(patternFilter || undefined, namespaceFilter || undefined);
};
@@ -250,6 +253,7 @@ const LiteTopicPage: React.FC = () => {
const handleNamespaceChange = (val: string | undefined) => {
const namespace = val || undefined;
setNamespaceFilter(namespace || '');
+ setCurrentPage(1);
void fetchData(patternFilter || undefined, namespace, { clear: true });
};
@@ -314,6 +318,9 @@ const LiteTopicPage: React.FC = () => {
return item.ttlStatus === ttlStatusFilter;
});
+ const lastPage = Math.max(1, Math.ceil(filteredTopicList.length / pageSize));
+ const clampedCurrentPage = Math.min(currentPage, lastPage);
+
// ─── Columns ─────────────────────────────────────────────────
const columns: ColumnsType<LiteTopicItem> = [
@@ -756,7 +763,10 @@ const LiteTopicPage: React.FC = () => {
aria-label={t('liteTopic.status')}
placeholder={t('liteTopic.status')}
value={ttlStatusFilter || undefined}
- onChange={(value) => setTTLStatusFilter(value || '')}
+ onChange={(value) => {
+ setTTLStatusFilter(value || '');
+ setCurrentPage(1);
+ }}
style={{ width: 160 }}
allowClear
options={[
@@ -780,7 +790,12 @@ const LiteTopicPage: React.FC = () => {
rowKey={(record) => JSON.stringify([record.namespace,
record.topicPattern])}
loading={loading}
pagination={{
- pageSize: 10,
+ current: clampedCurrentPage,
+ pageSize,
+ onChange: (page, nextPageSize) => {
+ setCurrentPage(page);
+ setPageSize(nextPageSize);
+ },
showTotal: (total) => t('liteTopic.total').replace('{total}',
String(total)),
showSizeChanger: true,
}}
diff --git a/web/src/pages/studio/__tests__/GroupManagement.test.tsx
b/web/src/pages/studio/__tests__/GroupManagement.test.tsx
index a42c6d804..ed4edffde 100644
--- a/web/src/pages/studio/__tests__/GroupManagement.test.tsx
+++ b/web/src/pages/studio/__tests__/GroupManagement.test.tsx
@@ -105,6 +105,43 @@ describe('GroupManagement Page', () => {
vi.restoreAllMocks();
});
+ it('returns to the first page when the group search changes', async () => {
+ vi.mocked(consumerService.listConsumerGroups).mockResolvedValue(
+ Array.from({ length: 11 }, (_, index) =>
+ makeGroup({ name: `group-${String(index).padStart(2, '0')}` }),
+ ),
+ );
+ const user = userEvent.setup();
+ const { container } = renderWithProviders(<GroupManagement />);
+
+ await screen.findByText('group-00');
+ await user.click(container.querySelector('.ant-pagination-next button')!);
+ expect(await screen.findByText('group-10')).toBeInTheDocument();
+ expect(screen.queryByText('group-00')).not.toBeInTheDocument();
+
+ await user.type(screen.getByPlaceholderText('搜索消费组'), 'group-00');
+ expect(await screen.findByText('group-00')).toBeInTheDocument();
+ });
+
+ it('clamps the current page when refreshed results have fewer pages', async
() => {
+ const initialGroups = Array.from({ length: 21 }, (_, index) =>
+ makeGroup({ name: `group-${String(index).padStart(2, '0')}` }),
+ );
+ vi.mocked(consumerService.listConsumerGroups)
+ .mockResolvedValueOnce(initialGroups)
+ .mockResolvedValueOnce(initialGroups.slice(0, 11));
+ const user = userEvent.setup();
+ const { container } = renderWithProviders(<GroupManagement />);
+
+ await screen.findByText('group-00');
+ await user.click(container.querySelector('.ant-pagination-item-3 a')!);
+ expect(await screen.findByText('group-20')).toBeInTheDocument();
+
+ await user.click(screen.getByRole('button', { name: /刷新/ }));
+ expect(await screen.findByText('group-10')).toBeInTheDocument();
+ expect(screen.queryByText('group-20')).not.toBeInTheDocument();
+ });
+
it('should render the page title', () => {
renderWithProviders(<GroupManagement />);
expect(screen.getByText('消费组管理')).toBeInTheDocument();
diff --git a/web/src/pages/studio/__tests__/LiteTopic.test.tsx
b/web/src/pages/studio/__tests__/LiteTopic.test.tsx
index 6deaea9ae..b3c0aa665 100644
--- a/web/src/pages/studio/__tests__/LiteTopic.test.tsx
+++ b/web/src/pages/studio/__tests__/LiteTopic.test.tsx
@@ -103,6 +103,30 @@ describe('LiteTopic Page', () => {
});
});
+ it('returns to the first page when the local TTL filter changes', async ()
=> {
+ apiMocks.queryLiteTopicList.mockResolvedValue([
+ ...Array.from({ length: 11 }, (_, index) => ({
+ namespace: 'default',
+ topicPattern: `active-${String(index).padStart(2, '0')}*`,
+ ttlStatus: 'ACTIVE' as const,
+ })),
+ { namespace: 'default', topicPattern: 'expired-*', ttlStatus: 'EXPIRED'
},
+ ]);
+ const user = userEvent.setup();
+ const { container } = renderPage();
+
+ await screen.findByText('active-00*');
+ await user.click(container.querySelector('.ant-pagination-next button')!);
+ expect(await screen.findByText('expired-*')).toBeInTheDocument();
+ expect(screen.queryByText('active-00*')).not.toBeInTheDocument();
+
+ await user.click(screen.getByRole('combobox', { name: '状态' }));
+ await user.click(
+ await screen.findByText('活跃', { selector:
'.ant-select-item-option-content' }),
+ );
+ expect(await screen.findByText('active-00*')).toBeInTheDocument();
+ });
+
it('displays the session POP progress returned by the API as a percentage',
async () => {
const user = userEvent.setup();
renderPage();