From 7084f65443a07062edbe874df96a1159bbae1367 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Tue, 22 Oct 2024 16:35:04 +0530 Subject: [PATCH] feat(CE): added new pagination format --- .../EnhancedPagination/PageChangeButton.tsx | 56 ++++++++++ .../EnhancedPagination/PageNumberItem.tsx | 42 ++++++++ .../EnhancedPagination/Pagination.tsx | 101 ++++++++++++++++++ .../__tests__/Pagination.test.tsx | 86 +++++++++++++++ ui/src/components/EnhancedPagination/index.ts | 1 + ui/src/components/ModelTable/ModelTable.tsx | 5 +- ui/src/services/common.ts | 6 +- .../Syncs/SyncRecords/SyncRecords.tsx | 32 ++---- 8 files changed, 303 insertions(+), 26 deletions(-) create mode 100644 ui/src/components/EnhancedPagination/PageChangeButton.tsx create mode 100644 ui/src/components/EnhancedPagination/PageNumberItem.tsx create mode 100644 ui/src/components/EnhancedPagination/Pagination.tsx create mode 100644 ui/src/components/EnhancedPagination/__tests__/Pagination.test.tsx create mode 100644 ui/src/components/EnhancedPagination/index.ts diff --git a/ui/src/components/EnhancedPagination/PageChangeButton.tsx b/ui/src/components/EnhancedPagination/PageChangeButton.tsx new file mode 100644 index 00000000..3a691edf --- /dev/null +++ b/ui/src/components/EnhancedPagination/PageChangeButton.tsx @@ -0,0 +1,56 @@ +import { Button, Icon } from '@chakra-ui/react'; +import { FiChevronLeft, FiChevronRight, FiChevronsLeft, FiChevronsRight } from 'react-icons/fi'; + +export enum PAGE_CHANGE_BUTTON_TYPE { + PREVIOUS = 'previous', + NEXT = 'next', + FIRST = 'first', + LAST = 'last', +} + +type PageButtonProps = { + type: PAGE_CHANGE_BUTTON_TYPE; + onClick: () => void; + isEnabled?: boolean; +}; + +const PageChangeButton = ({ type, onClick, isEnabled = false }: PageButtonProps) => { + const iconMap = { + [PAGE_CHANGE_BUTTON_TYPE.PREVIOUS]: { icon: FiChevronLeft, value: 'previous' }, + [PAGE_CHANGE_BUTTON_TYPE.FIRST]: { icon: FiChevronsLeft, value: 'first' }, + [PAGE_CHANGE_BUTTON_TYPE.LAST]: { icon: FiChevronsRight, value: 'last' }, + [PAGE_CHANGE_BUTTON_TYPE.NEXT]: { icon: FiChevronRight, value: 'next' }, + }; + + const icon = iconMap[type] || iconMap[PAGE_CHANGE_BUTTON_TYPE.NEXT]; + + return ( + + ); +}; + +export default PageChangeButton; diff --git a/ui/src/components/EnhancedPagination/PageNumberItem.tsx b/ui/src/components/EnhancedPagination/PageNumberItem.tsx new file mode 100644 index 00000000..c8ed9e58 --- /dev/null +++ b/ui/src/components/EnhancedPagination/PageNumberItem.tsx @@ -0,0 +1,42 @@ +import { Box, Text } from '@chakra-ui/react'; + +const PageNumberItem = ({ + isActive = false, + onClick, + value, + isEllipsis = false, +}: { + isActive?: boolean; + onClick?: () => void; + value?: number; + isEllipsis?: boolean; +}) => ( + {} : onClick} + _hover={{ backgroundColor: 'gray.400', cursor: 'pointer' }} + _disabled={{ + _hover: { cursor: 'not-allowed' }, + backgroundColor: 'gray.400', + }} + data-testid={isEllipsis ? 'ellipsis' : `page-number-${value}`} + > + + {isEllipsis ? '...' : value} + + +); + +export default PageNumberItem; diff --git a/ui/src/components/EnhancedPagination/Pagination.tsx b/ui/src/components/EnhancedPagination/Pagination.tsx new file mode 100644 index 00000000..2aee2d87 --- /dev/null +++ b/ui/src/components/EnhancedPagination/Pagination.tsx @@ -0,0 +1,101 @@ +import { LinksType } from '@/services/common'; +import { Stack, Box } from '@chakra-ui/react'; +import PageNumberItem from './PageNumberItem'; +import PageChangeButton, { PAGE_CHANGE_BUTTON_TYPE } from './PageChangeButton'; + +const getPage = (url: string): number => { + const parser = new URLSearchParams(new URL(url).search); + return parseInt(parser.get('page') || '0'); +}; + +export type PaginationProps = { + links: LinksType; + currentPage: number; + handlePageChange: (pageNumber: number) => void; +}; + +const Pagination = ({ links, currentPage, handlePageChange }: PaginationProps) => { + const firstPage = getPage(links.first); + const lastPage = getPage(links.last); + + const renderPageNumbers = () => { + if (currentPage === firstPage) { + return ( + <> + handlePageChange(currentPage)} + /> + {lastPage > firstPage + 1 && } + {lastPage !== firstPage && ( + handlePageChange(lastPage)} /> + )} + + ); + } else if (currentPage === lastPage) { + return ( + <> + handlePageChange(firstPage)} /> + {lastPage > firstPage + 1 && } + handlePageChange(currentPage)} + /> + + ); + } else { + return ( + <> + handlePageChange(firstPage)} /> + {currentPage > firstPage + 1 && } + handlePageChange(currentPage)} + /> + {currentPage < lastPage - 1 && } + handlePageChange(lastPage)} /> + + ); + } + }; + + return ( + + + handlePageChange(firstPage)} + /> + handlePageChange(currentPage - 1)} + /> + {renderPageNumbers()} + handlePageChange(currentPage + 1)} + /> + handlePageChange(lastPage)} + /> + + + ); +}; + +export default Pagination; diff --git a/ui/src/components/EnhancedPagination/__tests__/Pagination.test.tsx b/ui/src/components/EnhancedPagination/__tests__/Pagination.test.tsx new file mode 100644 index 00000000..a41a037d --- /dev/null +++ b/ui/src/components/EnhancedPagination/__tests__/Pagination.test.tsx @@ -0,0 +1,86 @@ +import { fireEvent, render, screen } from '@testing-library/react'; +import { expect } from '@jest/globals'; +import '@testing-library/jest-dom'; + +import Pagination, { PaginationProps } from '../Pagination'; + +describe('Pagination', () => { + const mockHandlePageChange = jest.fn(); + const defaultProps: PaginationProps = { + links: { + self: 'http://localhost:3000/api/v1/connectors?', + first: 'http://localhost:3000/api/v1/connectors?page=1&per_page=10', + prev: null, + next: 'http://localhost:3000/api/v1/connectors?page=2&per_page=10', + last: 'http://localhost:3000/api/v1/connectors?page=10&per_page=10', + }, + currentPage: 5, + handlePageChange: mockHandlePageChange, + }; + + beforeEach(() => { + mockHandlePageChange.mockClear(); + }); + + it('should render correct page numbers for middle page', () => { + render(); + expect(screen.getByTestId('page-number-1')).toBeTruthy(); + expect(screen.getByTestId('page-number-5')).toBeTruthy(); + expect(screen.getByTestId('page-number-10')).toBeTruthy(); + }); + + it('should render correct page numbers for first page', () => { + render(); + expect(screen.getByTestId('page-number-1')).toBeTruthy(); + expect(screen.getByTestId('page-number-10')).toBeTruthy(); + expect(screen.queryByTestId('page-number-5')).not.toBeTruthy(); + }); + + it('should render correct page numbers for last page', () => { + render(); + expect(screen.getByTestId('page-number-1')).toBeTruthy(); + expect(screen.getByTestId('page-number-10')).toBeTruthy(); + }); + + it('should call handlePageChange with correct page number when a page number is clicked', () => { + render(); + const pageButton = screen.getByTestId('page-number-1'); + fireEvent.click(pageButton); + expect(mockHandlePageChange).toHaveBeenCalledWith(1); + }); + + it('should disable first and previous buttons on first page', () => { + render(); + + expect(screen.getByTestId('page-change-first')).toHaveProperty('disabled', true); + expect(screen.getByTestId('page-change-previous')).toHaveProperty('disabled', true); + }); + + it('should disable next and last buttons on last page', () => { + render(); + + expect(screen.getByTestId('page-change-next')).toHaveProperty('disabled', true); + expect(screen.getByTestId('page-change-last')).toHaveProperty('disabled', true); + }); + + it('should call handlePageChange with correct page number when navigation buttons are clicked', () => { + render(); + fireEvent.click(screen.getByTestId('page-change-first')); + expect(mockHandlePageChange).toHaveBeenCalledWith(1); + + fireEvent.click(screen.getByTestId('page-change-previous')); + expect(mockHandlePageChange).toHaveBeenCalledWith(4); + + fireEvent.click(screen.getByTestId('page-change-next')); + expect(mockHandlePageChange).toHaveBeenCalledWith(6); + + fireEvent.click(screen.getByTestId('page-change-last')); + expect(mockHandlePageChange).toHaveBeenCalledWith(10); + }); + + it('should render ellipsis correctly', () => { + render(); + const ellipses = screen.getAllByText('...'); + expect(ellipses).toHaveLength(2); + }); +}); diff --git a/ui/src/components/EnhancedPagination/index.ts b/ui/src/components/EnhancedPagination/index.ts new file mode 100644 index 00000000..eaef04eb --- /dev/null +++ b/ui/src/components/EnhancedPagination/index.ts @@ -0,0 +1 @@ +export { default } from './Pagination'; diff --git a/ui/src/components/ModelTable/ModelTable.tsx b/ui/src/components/ModelTable/ModelTable.tsx index a8984ff5..bc3920d1 100644 --- a/ui/src/components/ModelTable/ModelTable.tsx +++ b/ui/src/components/ModelTable/ModelTable.tsx @@ -1,10 +1,11 @@ import GenerateTable from '@/components/Table/Table'; -import { getAllModels, APIData } from '@/services/models'; +import { getAllModels, GetAllModelsResponse } from '@/services/models'; import { addIconDataToArray, ConvertToTableData } from '@/utils'; import NoModels from '@/views/Models/NoModels'; import Loader from '@/components/Loader'; import useQueryWrapper from '@/hooks/useQueryWrapper'; import { useStore } from '@/stores'; +import { ApiResponse } from '@/services/common'; type ModelTableProps = { handleOnRowClick: (args: unknown) => void; @@ -13,7 +14,7 @@ type ModelTableProps = { const ModelTable = ({ handleOnRowClick }: ModelTableProps): JSX.Element => { const activeWorkspaceId = useStore((state) => state.workspaceId); - const { data } = useQueryWrapper( + const { data } = useQueryWrapper, Error>( ['models', activeWorkspaceId], () => getAllModels({ type: 'data' }), { diff --git a/ui/src/services/common.ts b/ui/src/services/common.ts index 68465e0d..b646148b 100644 --- a/ui/src/services/common.ts +++ b/ui/src/services/common.ts @@ -2,11 +2,11 @@ export * from '@/services/axios'; export type APIRequestMethod = 'get' | 'post' | 'put' | 'delete'; -type LinksType = { +export type LinksType = { first: string; last: string; - next: string; - prev: string; + next: string | null; + prev: string | null; self: string; }; diff --git a/ui/src/views/Activate/Syncs/SyncRecords/SyncRecords.tsx b/ui/src/views/Activate/Syncs/SyncRecords/SyncRecords.tsx index 5a228751..f87a9a97 100644 --- a/ui/src/views/Activate/Syncs/SyncRecords/SyncRecords.tsx +++ b/ui/src/views/Activate/Syncs/SyncRecords/SyncRecords.tsx @@ -7,7 +7,6 @@ import { getSyncRecords } from '@/services/syncs'; import Loader from '@/components/Loader'; import ContentContainer from '@/components/ContentContainer'; -import Pagination from '@/components/Pagination'; import SyncRunEmptyImage from '@/assets/images/empty-state-illustration.svg'; import { SyncRecordsTopBar } from './SyncRecordsTopBar'; @@ -21,6 +20,7 @@ import { SyncRecordResponse } from '@/views/Activate/Syncs/types'; import DataTable from '@/components/DataTable'; import { SyncRecordsColumns, useDynamicSyncColumns } from './SyncRecordsColumns'; +import Pagination from '@/components/EnhancedPagination'; const SyncRecords = (): JSX.Element => { const [searchParams, setSearchParams] = useSearchParams(); @@ -81,18 +81,6 @@ const SyncRecords = (): JSX.Element => { } }, [isFilteredSyncRecordsError, toast]); - const handleNextPage = () => { - if (filteredSyncRunRecords?.links?.next) { - setCurrentPage((prevPage) => prevPage + 1); - } - }; - - const handlePrevPage = () => { - if (filteredSyncRunRecords?.links?.prev) { - setCurrentPage((prevPage) => Math.max(prevPage - 1, 1)); - } - }; - const handleStatusTabChange = (status: SyncRecordStatus) => { setCurrentPage(1); setCurrentStatusTab(status); @@ -143,14 +131,16 @@ const SyncRecords = (): JSX.Element => { - - + + {filteredSyncRunRecords.links ? ( + + ) : ( + <>Pagination unavailable. + )} )}