diff --git a/frontend/src/components/pages/topics/Tab.Consumers.tsx b/frontend/src/components/pages/topics/Tab.Consumers.tsx index 398de1789..e6f1cc713 100644 --- a/frontend/src/components/pages/topics/Tab.Consumers.tsx +++ b/frontend/src/components/pages/topics/Tab.Consumers.tsx @@ -27,19 +27,17 @@ import { editQuery } from '../../../utils/queryHelper'; type TopicConsumersProps = { topic: Topic }; export const TopicConsumers: FC = observer(({ topic }) => { - const paginationParams = usePaginationParams(uiState.topicSettings.consumerPageSize); - let consumers = api.topicConsumers.get(topic.topicName); const isLoading = consumers === null; - if(isLoading) { return DefaultSkeleton; } - if (!consumers) { consumers = []; } + const paginationParams = usePaginationParams(uiState.topicSettings.consumerPageSize, consumers.length); + return ( data={consumers} diff --git a/frontend/src/components/pages/topics/Tab.Messages/index.tsx b/frontend/src/components/pages/topics/Tab.Messages/index.tsx index 94bd8baa9..4ca3c63ba 100644 --- a/frontend/src/components/pages/topics/Tab.Messages/index.tsx +++ b/frontend/src/components/pages/topics/Tab.Messages/index.tsx @@ -566,7 +566,7 @@ export class TopicMessageView extends Component { } MessageTable = observer(() => { - const paginationParams = usePaginationParams(uiState.topicSettings.searchParams.pageSize) + const paginationParams = usePaginationParams(uiState.topicSettings.searchParams.pageSize, this.messageSource.data.length) const [showPreviewSettings, setShowPreviewSettings] = React.useState(false); const tsFormat = uiState.topicSettings.previewTimestamps; diff --git a/frontend/src/components/pages/topics/Tab.Partitions.tsx b/frontend/src/components/pages/topics/Tab.Partitions.tsx index 526e717ec..a79871c0e 100644 --- a/frontend/src/components/pages/topics/Tab.Partitions.tsx +++ b/frontend/src/components/pages/topics/Tab.Partitions.tsx @@ -29,8 +29,6 @@ type TopicPartitionsProps = { }; export const TopicPartitions: FC = observer(({topic}) => { - const paginationParams = usePaginationParams(uiState.topicSettings.partitionPageSize); - let partitions = api.topicPartitions.get(topic.topicName); if (partitions === undefined) return DefaultSkeleton; if (partitions === null) partitions = []; // todo: show the error (if one was reported); @@ -43,6 +41,8 @@ export const TopicPartitions: FC = observer(({topic}) => { ); + const paginationParams = usePaginationParams(uiState.topicSettings.partitionPageSize, partitions.length); + return ( <> {warning} diff --git a/frontend/src/components/pages/topics/Topic.List.tsx b/frontend/src/components/pages/topics/Topic.List.tsx index 84c03c990..d8c4ce9db 100644 --- a/frontend/src/components/pages/topics/Topic.List.tsx +++ b/frontend/src/components/pages/topics/Topic.List.tsx @@ -196,7 +196,7 @@ class TopicList extends PageComponent { export default TopicList; const TopicsTable: FC<{ topics: Topic[], onDelete: (record: Topic) => void }> = ({ topics, onDelete }) => { - const paginationParams = usePaginationParams(uiSettings.topicList.pageSize) + const paginationParams = usePaginationParams(uiSettings.topicList.pageSize, topics.length) return ( diff --git a/frontend/src/hooks/usePaginationParams.test.ts b/frontend/src/hooks/usePaginationParams.test.ts index 14163559a..9a91d8b59 100644 --- a/frontend/src/hooks/usePaginationParams.test.ts +++ b/frontend/src/hooks/usePaginationParams.test.ts @@ -11,22 +11,34 @@ jest.mock('react-router-dom', () => ({ describe('usePaginationParams', () => { it('returns default values when URL parameters are absent', () => { (useLocation as jest.Mock).mockReturnValue({ search: '' }); - const { result } = renderHook(() => usePaginationParams()); + // Assuming a total data length of 100 for testing + const totalDataLength = 100; + const { result } = renderHook(() => usePaginationParams(10, totalDataLength)); expect(result.current.pageSize).toBe(10); expect(result.current.pageIndex).toBe(0); }); it('parses pageSize and pageIndex from URL parameters', () => { - (useLocation as jest.Mock).mockReturnValue({ search: '?pageSize=5&page=2' }); - const { result } = renderHook(() => usePaginationParams()); - expect(result.current.pageSize).toBe(5); + (useLocation as jest.Mock).mockReturnValue({ search: '?pageSize=20&page=2' }); + const totalDataLength = 100; + const { result } = renderHook(() => usePaginationParams(10, totalDataLength)); + expect(result.current.pageSize).toBe(20); expect(result.current.pageIndex).toBe(2); }); it('uses defaultPageSize when pageSize is not in URL', () => { (useLocation as jest.Mock).mockReturnValue({ search: '?page=3' }); - const { result } = renderHook(() => usePaginationParams(15)); + const totalDataLength = 150; + const { result } = renderHook(() => usePaginationParams(15, totalDataLength)); expect(result.current.pageSize).toBe(15); expect(result.current.pageIndex).toBe(3); }); + + it('adjusts pageIndex if it exceeds calculated totalPages', () => { + (useLocation as jest.Mock).mockReturnValue({ search: '?pageSize=10&page=20' }); // pageIndex is out of range + const totalDataLength = 50; // Only 5 pages available + const { result } = renderHook(() => usePaginationParams(10, totalDataLength)); + expect(result.current.pageSize).toBe(10); + expect(result.current.pageIndex).toBe(4); + }); }); diff --git a/frontend/src/hooks/usePaginationParams.ts b/frontend/src/hooks/usePaginationParams.ts index 915f9ca5a..d865cb283 100644 --- a/frontend/src/hooks/usePaginationParams.ts +++ b/frontend/src/hooks/usePaginationParams.ts @@ -10,22 +10,29 @@ import { useLocation } from 'react-router-dom'; * 'pageIndex' defaults to 0 if not present in the URL. * * @param {number} [defaultPageSize=10] - The default number of items per page if not specified in the URL. + * @param {number} totalDataLength - The total length of the data to paginate over. * @returns {{ pageSize: number; pageIndex: number }} An object containing the pageSize and pageIndex. * * @example * // In a component using react-router * const { pageSize, pageIndex } = usePaginationParams(20); */ -const usePaginationParams = (defaultPageSize: number = 10): { pageSize: number; pageIndex: number } => { +const usePaginationParams = (defaultPageSize: number = 10, totalDataLength: number): { pageSize: number; pageIndex: number } => { const { search} = useLocation(); return useMemo(() => { - const searchParams = new URLSearchParams(search) + const searchParams = new URLSearchParams(search); + const pageSize = searchParams.has('pageSize') ? Number(searchParams.get('pageSize')) : defaultPageSize; + const pageIndex = searchParams.has('page') ? Number(searchParams.get('page')) : 0; + const totalPages = Math.ceil(totalDataLength / pageSize); + + const boundedPageIndex = Math.max(0, Math.min(pageIndex, totalPages - 1)); + return { - pageSize: searchParams.has('pageSize') ? Number(searchParams.get('pageSize')) : defaultPageSize, - pageIndex: searchParams.has('page') ? Number(searchParams.get('page')) : 0, + pageSize, + pageIndex: boundedPageIndex, } - }, [search, defaultPageSize]) + }, [search, defaultPageSize, totalDataLength]) }; export default usePaginationParams;