This is an automated email from the ASF dual-hosted git repository. ppawar pushed a commit to branch ATLAS-5036 in repository https://gitbox.apache.org/repos/asf/atlas.git
commit 10add959dcbc980e7fe1918ec455aa401d96af3f Author: Prasad Pawar <[email protected]> AuthorDate: Mon Sep 8 16:40:28 2025 +0530 ATLAS-5036: [React UI] Pagination Issue: 'Go to Page' input resets and Incorrect page number Display with font & alignment inconsistencies --- dashboard/src/components/Table/TableLayout.tsx | 82 ++- dashboard/src/components/Table/TablePagination.tsx | 582 +++++++++++++-------- dashboard/src/components/commonComponents.tsx | 24 + dashboard/src/models/tableLayoutType.ts | 4 + dashboard/src/styles/table.scss | 25 +- dashboard/src/utils/Enum.ts | 18 + dashboard/src/utils/Messages.ts | 10 +- dashboard/src/utils/Utils.ts | 2 +- .../views/Administrator/Audits/AdminAuditTable.tsx | 11 +- .../DetailPage/EntityDetailTabs/AuditsTab.tsx | 16 +- .../EntityDetailTabs/ClassificationsTab.tsx | 3 + .../DetailPage/EntityDetailTabs/ProfileTab.tsx | 12 +- .../EntityDetailTabs/ReplicationAuditTab.tsx | 3 +- .../src/views/SearchResult/RelationShipSearch.tsx | 6 +- dashboard/src/views/SearchResult/SearchResult.tsx | 57 +- .../src/views/SideBar/SideBarTree/SideBarTree.tsx | 6 +- 16 files changed, 534 insertions(+), 327 deletions(-) diff --git a/dashboard/src/components/Table/TableLayout.tsx b/dashboard/src/components/Table/TableLayout.tsx index d4c03d115..300f7d328 100644 --- a/dashboard/src/components/Table/TableLayout.tsx +++ b/dashboard/src/components/Table/TableLayout.tsx @@ -43,7 +43,6 @@ import { } from "@tanstack/react-table"; import { FC, useEffect, useMemo, useState } from "react"; import TableFilter from "./TableFilters"; -import TablePagination from "./TablePagination"; import ArrowUpwardOutlinedIcon from "@mui/icons-material/ArrowUpwardOutlined"; import ArrowDownwardOutlinedIcon from "@mui/icons-material/ArrowDownwardOutlined"; import SwapVertOutlinedIcon from "@mui/icons-material/SwapVertOutlined"; @@ -75,6 +74,7 @@ import AddOutlinedIcon from "@mui/icons-material/AddOutlined"; import TableRowsLoader from "./TableLoader"; import AddTag from "@views/Classification/AddTag"; import FilterQuery from "@components/FilterQuery"; +import TablePagination from "./TablePagination"; interface IndeterminateCheckboxProps extends Omit<CheckboxProps, "ref"> { indeterminate?: boolean; @@ -332,6 +332,7 @@ const TableLayout: FC<TableProps> = ({ isFetching, defaultColumnVisibility, pageCount, + totalCount, onClickRow, emptyText, defaultColumnParams, @@ -352,18 +353,23 @@ const TableLayout: FC<TableProps> = ({ showPagination, setUpdateTable, isfilterQuery, - isClientSidePagination + isClientSidePagination, + isEmptyData, + setIsEmptyData, + showGoToPage }) => { let defaultHideColumns = { ...defaultColumnVisibility }; const location = useLocation(); const memoizedData = useMemo(() => data, [data]); const memoizedColumns = useMemo(() => columns, [columns]); const [searchParams] = useSearchParams(); - const [pagination, setPagination] = useState<PaginationState>({ pageIndex: 0, pageSize: 25 }); + + const [goToPageVal, setGoToPageVal] = useState<any>(""); + const [rowSelection, setRowSelection] = useState({}); const [sorting, setSorting] = useState<SortingState>( !isEmpty(defaultSortCol) ? defaultSortCol : [] @@ -384,16 +390,11 @@ const TableLayout: FC<TableProps> = ({ const { getHeaderGroups, getRowModel, - firstPage, - getCanPreviousPage, - previousPage, - nextPage, - getCanNextPage, - lastPage, setPageIndex, getPageCount, + nextPage, + previousPage, setPageSize, - getRowCount, resetSorting, resetRowSelection, getIsAllRowsSelected, @@ -414,7 +415,9 @@ const TableLayout: FC<TableProps> = ({ onSortingChange: setSorting, onColumnOrderChange: setColumnOrder, getSortedRowModel: getSortedRowModel(), - getPaginationRowModel: getPaginationRowModel(), + getPaginationRowModel: isClientSidePagination + ? getPaginationRowModel() + : undefined, onPaginationChange: setPagination, state: { columnVisibility: columnVisibilityParams @@ -432,14 +435,17 @@ const TableLayout: FC<TableProps> = ({ }); useEffect(() => { - if (!isEmpty(fetchData)) { - fetchData({ pagination, sorting }); + if (typeof fetchData === "function") { + fetchData({ + pagination, + sorting + }); } }, [ fetchData, pagination.pageIndex, pagination.pageSize, - !clientSideSorting && sorting + clientSideSorting ? null : sorting ]); function handleDragEnd(event: DragEndEvent) { @@ -459,30 +465,14 @@ const TableLayout: FC<TableProps> = ({ useSensor(KeyboardSensor, {}) ); + const [, setSearchParams] = useSearchParams(); + useEffect(() => { resetSorting(true); resetRowSelection(true); setRowSelection({}); setSorting(!isEmpty(defaultSortCol) ? defaultSortCol : []); - // setColumnVisibility(defaultHideColumns); - }, [typeParam]); - - // if (fetchData != undefined) { - - // } - - let currentPageOffset = searchParams.get("pageOffset") || 0; - let isFirstPage = currentPageOffset == 0; - - // const filteredParams = Array.from(searchParams.entries()) - // .filter(([key]) => ["type", "tag", "term"].includes(key)) - // .map(([key, value]) => ({ - // keyName: - // key === "tag" - // ? "Classification" - // : key.charAt(0).toUpperCase() + key.slice(1), - // value - // })); + }, [typeParam, defaultSortCol, setSearchParams]); const handleCloseTagModal = () => { setTagModal(false); @@ -669,31 +659,27 @@ const TableLayout: FC<TableProps> = ({ </MuiTable> </DndContext> </TableContainer> - {/* {noDataFound && ( - <Stack my={2} textAlign="center"> - {emptyText} - </Stack> - )} */} {showPagination && ( <TablePagination - firstPage={firstPage} - getCanPreviousPage={getCanPreviousPage} - previousPage={previousPage} - nextPage={nextPage} - getCanNextPage={getCanNextPage} - lastPage={lastPage} + isServerSide={!isClientSidePagination} getPageCount={getPageCount} setPageIndex={setPageIndex} setPageSize={setPageSize} + nextPage={nextPage} + previousPage={previousPage} getRowModel={getRowModel} - getRowCount={getRowCount} pagination={pagination} - memoizedData={memoizedData} - isFirstPage={isFirstPage} setRowSelection={setRowSelection} - isClientSidePagination={isClientSidePagination} + memoizedData={memoizedData} + isFirstPage={pagination.pageIndex === 0} setPagination={setPagination} + goToPageVal={goToPageVal} + setGoToPageVal={setGoToPageVal} + isEmptyData={isEmptyData} + setIsEmptyData={setIsEmptyData} + showGoToPage={showGoToPage} + totalCount={totalCount} /> )} </Paper> diff --git a/dashboard/src/components/Table/TablePagination.tsx b/dashboard/src/components/Table/TablePagination.tsx index 989708a28..2820b47f5 100644 --- a/dashboard/src/components/Table/TablePagination.tsx +++ b/dashboard/src/components/Table/TablePagination.tsx @@ -15,7 +15,7 @@ * limitations under the License. */ -import * as React from "react"; +import { useState, useRef, useEffect } from "react"; import { Autocomplete, FormControl, @@ -26,16 +26,21 @@ import { Paper, Stack, TextField, - Typography, - createFilterOptions, - styled + Typography } from "@mui/material"; - import { useTheme } from "@emotion/react"; import { KeyboardArrowLeft, KeyboardArrowRight } from "@mui/icons-material"; import { LightTooltip } from "../muiComponents"; import { useLocation, useNavigate } from "react-router-dom"; import { isEmpty } from "../../utils/Utils"; +import Messages from "@utils/Messages"; +import { toast } from "react-toastify"; +import { GetNumberSuffix } from "@components/commonComponents"; +import { pageSizeOptions } from "@utils/Enum"; +import { createFilterOptions } from "@mui/material/Autocomplete"; +import { styled } from "@mui/material/styles"; + +const filter = createFilterOptions<any>(); export const StyledPagination = styled(Pagination)` display: flex; @@ -44,46 +49,31 @@ export const StyledPagination = styled(Pagination)` `; interface PaginationProps { - firstPage?: any; - getCanPreviousPage?: any; - previousPage?: any; - nextPage?: any; - getCanNextPage?: any; - lastPage?: any; - getPageCount?: any; - setPageIndex?: any; - setPageSize?: any; - getRowModel?: any; - getRowCount?: any; - pagination?: any; - setRowSelection?: any; - memoizedData?: any; - isFirstPage?: any; - isClientSidePagination?: any; - setPagination?: any; -} -interface FilmOptionType { - inputValue?: string; - label: string; + isServerSide?: boolean; + getPageCount?: () => number; + previousPage?: () => void; + nextPage?: () => void; + setPageIndex?: (index: number) => void; + setPageSize?: (size: number) => void; + getRowModel?: () => any; + pagination: { pageIndex: number; pageSize: number }; + setRowSelection?: (selection: any) => void; + memoizedData: any[]; + isFirstPage?: boolean; + setPagination?: (pagination: any) => void; + goToPageVal?: string; + setGoToPageVal?: React.Dispatch<React.SetStateAction<string>>; + isEmptyData?: boolean; + setIsEmptyData?: any; + showGoToPage?: boolean; + totalCount?: number; } -const optionsVal = [ - { label: "25" }, - { label: "50" }, - { label: "100" }, - { label: "150" }, - { label: "200" }, - { label: "250" }, - { label: "300" }, - { label: "350" }, - { label: "400" }, - { label: "450" }, - { label: "500" } -]; -const filter = createFilterOptions<FilmOptionType>(); - -const options: readonly FilmOptionType[] = optionsVal; const TablePagination: React.FC<PaginationProps> = ({ + isServerSide = false, + getPageCount, + previousPage, + nextPage, setPageIndex, setPageSize, getRowModel, @@ -91,45 +81,268 @@ const TablePagination: React.FC<PaginationProps> = ({ setRowSelection, memoizedData, isFirstPage, - setPagination + setPagination, + goToPageVal, + setGoToPageVal, + isEmptyData, + setIsEmptyData, + showGoToPage = false, + totalCount }) => { const theme: any = useTheme(); const location = useLocation(); const navigate = useNavigate(); - const searchParams: any = new URLSearchParams(location.search); - const [inputVal, setInputVal] = React.useState<any>(""); - const [value, setValue] = React.useState<any>( - searchParams.get("pageLimit") != null - ? searchParams.get("pageLimit") - : options[0].label + const searchParams = new URLSearchParams(location.search); + const { pageIndex, pageSize } = pagination; + + const [value, setValue] = useState<any>({ + label: + searchParams.get("pageLimit") ?? + pageSize.toString() ?? + pageSizeOptions[0].label + }); + const [limit, setLimit] = useState<number>( + searchParams.get("pageLimit") + ? Number(searchParams.get("pageLimit")) + : pageSize ); - const { pageSize, pageIndex } = pagination; - const handleChange = (newValue: any) => { - setPageSize(newValue.label); - setRowSelection({}); - searchParams.delete("pageOffset"); - searchParams.set("pageLimit", newValue.label); - navigate({ search: searchParams.toString() }); - }; - const [_searchState, setSearchState] = React.useState( - Object.fromEntries(searchParams) + const [pageFrom, setPageFrom] = useState<number>( + isServerSide && searchParams.get("pageOffset") + ? Number(searchParams.get("pageOffset")) + 1 + : isServerSide + ? 1 + : pageIndex * pageSize + 1 ); + const [pageTo, setPageTo] = useState<number>( + isServerSide && searchParams.get("pageOffset") + ? Number(searchParams.get("pageOffset")) + Number(limit) + : isServerSide + ? Number(limit) + : Math.min((pageIndex + 1) * pageSize, getRowModel?.().rows.length || 0) + ); + const [offset, setOffset] = useState<number>( + isServerSide && searchParams.get("pageOffset") + ? Number(searchParams.get("pageOffset")) + : pageIndex * pageSize + ); + const [activePage, setActivePage] = useState<number>( + isServerSide ? Math.floor(offset / Number(limit)) + 1 : pageIndex + 1 + ); + + const [pendingGoToPageVal, setPendingGoToPageVal] = useState<string>(""); + const [goToPageTrigger, setGoToPageTrigger] = useState<string>(""); + const toastId = useRef<any>(null); + + useEffect(() => { + if (isServerSide) { + setValue({ label: searchParams.get("pageLimit") ?? limit.toString() }); + setLimit(Number(searchParams.get("pageLimit") ?? limit)); + setOffset(Number(searchParams.get("pageOffset") ?? pageIndex * pageSize)); + setPageFrom( + Number(searchParams.get("pageOffset") ?? pageIndex * pageSize) + 1 + ); + setPageTo( + Number(searchParams.get("pageOffset") ?? pageIndex * pageSize) + + Number(limit) + ); + } + }, [isServerSide, location.search, limit, pageIndex, pageSize]); + + // Do not force offset to 0 based solely on isFirstPage; rely on URL (pageOffset) + + useEffect(() => { + if (isServerSide) { + const sp = new URLSearchParams(location.search); + const effLimit = Number(sp.get("pageLimit") ?? limit); + const effOffset = Number(sp.get("pageOffset") ?? 0); + setActivePage(Math.floor(effOffset / effLimit) + 1); + } else { + setActivePage(pageIndex + 1); + } + }, [isServerSide, location.search, limit, pageIndex]); + + const handlePageSizeChange = (newValue: any) => { + const newPageSize: any = Number(newValue.label); + setValue({ label: newValue.label }); + if (isServerSide) { + setLimit(newPageSize); + setPageFrom(1); + setPageTo(newPageSize); + setActivePage(1); + setOffset(0); + searchParams.delete("pageOffset"); + searchParams.set("pageLimit", newPageSize); + navigate({ search: searchParams.toString() }); + setRowSelection?.({}); + setPagination?.((prev: any) => ({ ...prev, pageIndex: 0, pageSize: newPageSize })); + // Clear Go-to state on page size change + setGoToPageVal?.(""); + setPendingGoToPageVal(""); + setGoToPageTrigger(""); + } else { + setPageSize?.(newPageSize); + setPageIndex?.(0); + setLimit(newPageSize); + setPageFrom(1); + setPageTo(Math.min(newPageSize, getRowModel?.().rows.length || 0)); + // Clear Go-to state on page size change + setGoToPageVal?.(""); + setPendingGoToPageVal(""); + setGoToPageTrigger(""); + } + }; + + const handleGoToPage = () => { + const goToPage = parseInt(goToPageVal || pendingGoToPageVal); + if (isNaN(goToPage) || goToPage < 1) return; + + if (isServerSide) { + // Guard based on approximate total count if available: pages are 1..ceil(totalCount/limit) + if (typeof totalCount === "number" && totalCount >= 0) { + const maxPage = Math.max(1, Math.ceil(totalCount / Number(limit || 1))); + if (goToPage > maxPage) { + toast.dismiss(toastId.current); + toastId.current = toast.info( + <> + {Messages.search.noRecordForPage} + <b> + <GetNumberSuffix number={goToPage} sup={true} /> + </b>{" "} + page + </> + ); + setGoToPageVal?.(""); + setPendingGoToPageVal(""); + return; + } + } + const newOffset = (goToPage - 1) * limit; + if (newOffset === offset) { + toast.dismiss(toastId.current); + toastId.current = toast.info(`${Messages.search.onSamePage}`); + setGoToPageVal?.(""); + setPendingGoToPageVal(""); + return; + } + setOffset(newOffset); + setPageFrom(newOffset + 1); + setPageTo(newOffset + limit); + setActivePage(goToPage); + setPageIndex?.(goToPage - 1); + setRowSelection?.({}); + searchParams.set("pageOffset", newOffset.toString()); + navigate({ search: searchParams.toString() }); + setGoToPageVal?.(""); + setPendingGoToPageVal(""); + } else { + if (goToPage > (getPageCount?.() || Infinity)) { + toast.dismiss(toastId.current); + toastId.current = toast.info( + <> + {Messages.search.noRecordForPage} page "<strong>{goToPage}</strong>" + </> + ); + setGoToPageVal?.(""); + setPendingGoToPageVal(""); + setGoToPageTrigger(""); + return; + } + if (goToPage - 1 === pageIndex) { + toast.dismiss(toastId.current); + toastId.current = toast.info(`${Messages.search.onSamePage}`); + setGoToPageVal?.(""); + setPendingGoToPageVal(""); + setGoToPageTrigger(""); + return; + } + setPageIndex?.(goToPage - 1); + setPageFrom((goToPage - 1) * pageSize + 1); + setPageTo( + Math.min(goToPage * pageSize, getRowModel?.().rows.length || 0) + ); + setGoToPageVal?.(""); + setPendingGoToPageVal(""); + setGoToPageTrigger(""); + } + }; + + useEffect(() => { + if ((goToPageVal || goToPageTrigger || isEmptyData) && setPageIndex) { + handleGoToPage(); + if (typeof setIsEmptyData === "function") { + setIsEmptyData(false); + } + setGoToPageTrigger(""); + } + }, [goToPageVal, goToPageTrigger, isEmptyData]); + + const handlePreviousPage = () => { + if (isServerSide) { + const prevOffset = offset - limit; + const safePrevOffset = prevOffset < 0 ? 0 : prevOffset; + setOffset(safePrevOffset); + setPageFrom(safePrevOffset + 1); + setPageTo(safePrevOffset + limit); + setPagination?.((prev: any) => ({ + ...prev, + pageIndex: prev.pageIndex - 1 + })); + setRowSelection?.({}); + // Always reflect new offset in URL (including 0) so fetch effect triggers correctly + searchParams.set("pageOffset", safePrevOffset.toString()); + navigate({ search: searchParams.toString() }); + } else { + previousPage?.(); + setPageFrom(pageIndex * pageSize + 1); + setPageTo( + Math.min(pageIndex * pageSize, getRowModel?.().rows.length || 0) + ); + } + setGoToPageVal?.(""); + }; + + const handleNextPage = () => { + if (isServerSide) { + const nextOffset = offset + limit; + setOffset(nextOffset); + setPageFrom(nextOffset + 1); + setPageTo(nextOffset + limit); + setPagination?.((prev: any) => ({ + ...prev, + pageIndex: prev.pageIndex + 1 + })); + setRowSelection?.({}); + searchParams.set("pageOffset", nextOffset.toString()); + navigate({ search: searchParams.toString() }); + } else { + nextPage?.(); + setPageFrom((pageIndex + 1) * pageSize + 1); + setPageTo( + Math.min((pageIndex + 2) * pageSize, getRowModel?.().rows.length || 0) + ); + } + setGoToPageVal?.(""); + }; - React.useEffect(() => { - setSearchState(Object.fromEntries(searchParams)); - }, [location.search]); + // In server-side mode, rely on computed offset to decide first page + const isPreviousDisabled = isServerSide ? offset === 0 : pageIndex === 0; + // In server-side mode, disable Next when fewer than limit rows returned (last page) + const isNextDisabled = isServerSide + ? (typeof totalCount === "number" && totalCount >= 0 + ? offset + limit >= totalCount + : memoizedData.length < limit) + : pageIndex + 1 >= (getPageCount?.() || Infinity); - let limit = - parseInt( - searchParams.get("pageLimit") !== undefined && - searchParams.get("pageLimit") !== null - ? searchParams.get("pageLimit") - : pageSize || "0", - 10 - ) || 0; - let pageFrom = isFirstPage ? 1 : pageSize * pageIndex + 1; - let pageTo = isFirstPage ? Number(limit) : pageSize * (Number(pageIndex) + 1); + const totalRows = getRowModel?.().rows.length || 0; + const displayFrom = isServerSide + ? pageFrom + : totalRows === 0 + ? 0 + : pageIndex * pageSize + 1; + const displayTo = isServerSide + ? pageTo + : Math.min((pageIndex + 1) * pageSize, totalRows); return ( <Stack @@ -143,10 +356,11 @@ const TablePagination: React.FC<PaginationProps> = ({ > <div> <span className="text-grey"> - Showing <u>{getRowModel()?.rows?.length.toLocaleString()} records</u>{" "} - From {pageFrom} - {pageTo} + Showing <u>{totalRows.toLocaleString()} records</u> From {displayFrom}{" "} + - {displayTo} </span> </div> + <div className="table-pagination-filters"> <Stack className="table-pagination-filters-box" @@ -157,7 +371,7 @@ const TablePagination: React.FC<PaginationProps> = ({ <Typography className="text-grey" whiteSpace="nowrap" - fontWeight="600" + fontWeight="400" lineHeight="32px" > Page Limit : @@ -167,163 +381,122 @@ const TablePagination: React.FC<PaginationProps> = ({ value={value} className="pagination-page-limit" disableClearable - onChange={(_event: any, newValue) => { + onChange={(_, newValue: any) => { if (typeof newValue === "string") { - setValue({ - label: newValue - }); - handleChange({ label: newValue }); - } else if (newValue && newValue.inputValue) { - setValue({ - label: newValue.inputValue - }); - handleChange({ label: newValue.inputValue }); + handlePageSizeChange({ label: newValue }); + } else if (newValue?.inputValue) { + handlePageSizeChange({ label: newValue.inputValue }); } else if (newValue) { - setValue(newValue); - handleChange(newValue); - } else { - const fallbackValue = { label: inputVal || "" }; - setValue(fallbackValue); - handleChange(fallbackValue); + handlePageSizeChange(newValue); } - setPagination((prev: { pageIndex: number }) => ({ - ...prev, - pageIndex: 0 - })); }} filterOptions={(options, params) => { const filtered = filter(options, params); - const { inputValue } = params; - const isExisting = options.some( (option) => inputValue === option.label ); - if (inputValue !== "" && !isExisting) { - filtered.push({ - inputValue, - label: `${inputValue}` - }); + if (inputValue !== "" && setPageSize && !isExisting) { + filtered.push({ inputValue, label: `${inputValue}` }); } - return filtered; }} - defaultValue={ - searchParams.get("pageLimit") != null && - searchParams.get("pageLimit") - } selectOnFocus clearOnBlur handleHomeEndKeys id="Page Limit:" - options={options} + options={pageSizeOptions} size="small" - getOptionLabel={(option) => { - if (typeof option === "string") { - return option; - } - if (option.inputValue) { - return option.inputValue; - } - return option.label; - }} + getOptionLabel={(option) => + typeof option === "string" + ? option + : option.inputValue || option.label + } renderOption={(props, option) => ( <MenuItem {...props} value={option.label}> {option.label} </MenuItem> )} sx={{ - width: "78px" + width: "78px", + "& .MuiOutlinedInput-root.MuiInputBase-sizeSmall .MuiAutocomplete-input": + { + padding: "0 4px !important", + height: "15px !important" + } }} freeSolo renderInput={(params) => <TextField type="number" {...params} />} /> </FormControl> </Stack> - {isFirstPage && - (!memoizedData.length || memoizedData.length < pagination.pageSize) ? ( - "" - ) : ( + + {(isServerSide + ? !isFirstPage || memoizedData.length >= limit + : memoizedData.length >= pageSize) && ( <> - <Stack className="table-pagination-filters-box"> - <Paper - component="form" - elevation={0} - className="table-pagination-gotopage-paper" - > - <InputBase - placeholder="Go to page:" - type="number" - inputProps={{ "aria-label": "Go to page:" }} - size="small" - onChange={(e: React.ChangeEvent<HTMLInputElement>) => { - const page = e.target.value - ? Number(e.target.value) - 1 - : 0; - setInputVal(page); - }} - className="table-pagination-gotopage-input" - defaultValue={inputVal} - /> - <LightTooltip title="Goto Page"> - <IconButton - type="button" + {showGoToPage && ( + <Stack className="table-pagination-filters-box"> + <Paper + component="form" + elevation={0} + className="table-pagination-gotopage-paper" + onSubmit={(e) => e.preventDefault()} + > + <InputBase + placeholder="Go to page:" + type="number" + inputProps={{ + "aria-label": "Go to page:", + style: { width: "100%" } + }} size="small" - className={`${ - !isEmpty(inputVal) - ? "cursor-pointer" - : "cursor-not-allowed" - } table-pagination-gotopage-button`} - aria-label="search" - onClick={() => { - if (!isEmpty(inputVal)) { - if (inputVal >= 1) { - setPageIndex(inputVal); - searchParams.set( - "pageOffset", - pagination.pageSize * inputVal - ); - navigate({ search: searchParams.toString() }); - } else { - setPageIndex(0); - searchParams.delete("pageOffset"); - navigate({ search: searchParams.toString() }); - } - setInputVal(""); - } else { - return; + onChange={(e) => { + setPendingGoToPageVal(e.target.value); + }} + onKeyUp={(e) => { + const goToPage = parseInt(e.currentTarget.value); + const anyEvent: any = e as any; + if ((anyEvent.key === "Enter" || anyEvent.keyCode === 13) && !isNaN(goToPage) && goToPage >= 1) { + setGoToPageVal?.(e.currentTarget.value); + setGoToPageTrigger(e.currentTarget.value); } - setRowSelection({}); }} - > - Go - </IconButton> - </LightTooltip> - </Paper> - </Stack> + className="table-pagination-gotopage-input" + value={pendingGoToPageVal} + /> + <LightTooltip title="Goto Page"> + <IconButton + type="button" + size="small" + className={`${ + !isEmpty(pendingGoToPageVal) + ? "cursor-pointer" + : "cursor-not-allowed" + } table-pagination-gotopage-button`} + aria-label="search" + onClick={() => { + if (!isEmpty(pendingGoToPageVal)) { + setGoToPageTrigger(pendingGoToPageVal); + handleGoToPage(); + } + }} + disabled={isEmpty(pendingGoToPageVal)} + > + Go + </IconButton> + </LightTooltip> + </Paper> + </Stack> + )} <Stack flexDirection="row" alignItems="center"> <LightTooltip title="Previous"> <IconButton size="small" - className="pagination-previous-btn" - onClick={() => { - setPagination((prev: { pageIndex: number }) => ({ - ...prev, - pageIndex: prev.pageIndex - 1 - })); - setRowSelection({}); - if (isFirstPage) { - searchParams.delete("pageOffset"); - } else { - searchParams.set( - "pageOffset", - `${pagination.pageSize * (pagination.pageIndex - 1)}` - ); - navigate({ search: searchParams.toString() }); - } - }} - disabled={isFirstPage} + className="pagination-page-change-btn" + onClick={handlePreviousPage} + disabled={isPreviousDisabled} aria-label="previous page" > {theme.direction === "rtl" ? ( @@ -333,30 +506,19 @@ const TablePagination: React.FC<PaginationProps> = ({ )} </IconButton> </LightTooltip> - <LightTooltip title={`Page ${Math.round(pageTo / limit)}`}> + + <LightTooltip title={`Page ${activePage}`}> <IconButton size="small" className="table-pagination-page"> - {Math.round(pageIndex + 1)} + {activePage} </IconButton> </LightTooltip> <LightTooltip title="Next"> <IconButton size="small" - className="pagination-next-btn" - onClick={() => { - setPagination((prev: { pageIndex: number }) => ({ - ...prev, - pageIndex: prev.pageIndex + 1 - })); - setRowSelection({}); - - searchParams.set( - "pageOffset", - `${pagination.pageSize * (pagination.pageIndex + 1)}` - ); - navigate({ search: searchParams.toString() }); - }} - disabled={memoizedData.length < limit} + className="pagination-page-change-btn" + onClick={handleNextPage} + disabled={isNextDisabled} aria-label="next page" > {theme.direction === "rtl" ? ( diff --git a/dashboard/src/components/commonComponents.tsx b/dashboard/src/components/commonComponents.tsx index 6db34530b..604b3c712 100644 --- a/dashboard/src/components/commonComponents.tsx +++ b/dashboard/src/components/commonComponents.tsx @@ -454,3 +454,27 @@ export const getValues = ( </span> ); }; + +export const GetNumberSuffix = (options: { number: any; sup?: boolean }) => { + if (options && options.number) { + let n = options.number; + let s = ["th", "st", "nd", "rd"]; + let v = n % 100; + let suffix = s[(v - 20) % 10] || s[v] || s[0]; + if (options.sup) { + return ( + <> + <>{n}</> + <sup>{suffix}</sup> + </> + ); + } else { + return ( + <> + {n} + {suffix} + </> + ); + } + } +}; diff --git a/dashboard/src/models/tableLayoutType.ts b/dashboard/src/models/tableLayoutType.ts index 96e291585..0baeb064f 100644 --- a/dashboard/src/models/tableLayoutType.ts +++ b/dashboard/src/models/tableLayoutType.ts @@ -26,6 +26,7 @@ export interface TableProps { skeletonHeight?: number; headerComponent?: JSX.Element; pageCount?: number; + totalCount?: number; defaultColumnVisibility?: any; page?: (page: number) => void; onClickRow?: (cell: Cell<any, unknown>, row: Row<any>) => void; @@ -52,4 +53,7 @@ export interface TableProps { setUpdateTable?: any; isfilterQuery?: any; isClientSidePagination?: boolean; + isEmptyData?: boolean; + setIsEmptyData?: React.Dispatch<React.SetStateAction<boolean>>; + showGoToPage?: boolean; } diff --git a/dashboard/src/styles/table.scss b/dashboard/src/styles/table.scss index f9dce88a8..a8fc0c7d7 100644 --- a/dashboard/src/styles/table.scss +++ b/dashboard/src/styles/table.scss @@ -91,18 +91,22 @@ align-items: center; width: 150px; border: 1px solid rgba(0, 0, 0, 0.23); - height: 32px; + height: 27px; } -.pagination-previous-btn { - height: 32px; - width: 32px; +.pagination-page-change-btn { + height: 24px; + width: 24px; } .table-pagination-gotopage-input { margin-left: 5px; flex: 1px; } +.table-pagination-gotopage-input input { + padding: 0 4px !important; +} + .table-pagination-gotopage-button { padding: 6px !important; border-radius: 0 !important; @@ -111,20 +115,11 @@ } .table-pagination-page { border-radius: 4px !important; - height: 24px; - width: 24px; + height: 20px; color: #37bb9b !important; border: 1px solid #37bb9b !important; margin: 0 0.5rem !important; -} - -.pagination-page-limit .MuiInputBase-fullWidth { - padding-top: 3.5px !important; - padding-bottom: 3.5px !important; -} - -.pagination-page-limit input { - height: 20px; + font-size: 0.875rem !important; } /* table-flters */ diff --git a/dashboard/src/utils/Enum.ts b/dashboard/src/utils/Enum.ts index ffb6fcf90..f95ea8add 100644 --- a/dashboard/src/utils/Enum.ts +++ b/dashboard/src/utils/Enum.ts @@ -473,3 +473,21 @@ export const defaultType = [ "array<long>", "array<date>" ]; + +export const optionsVal = [ + "25", + "50", + "100", + "150", + "200", + "250", + "300", + "350", + "400", + "450", + "500" +]; + +export const pageSizeOptions = optionsVal.map((x: string) => ({ + label: x +})); diff --git a/dashboard/src/utils/Messages.ts b/dashboard/src/utils/Messages.ts index bdb0d900d..8c1a424b8 100644 --- a/dashboard/src/utils/Messages.ts +++ b/dashboard/src/utils/Messages.ts @@ -15,6 +15,12 @@ * limitations under the License. */ -export const Messages = { - defaultErrorMessage: "Something went wrong" +const Messages = { + defaultErrorMessage: "Something went wrong", + search: { + noRecordForPage: "No record found at ", + onSamePage: "You are on the same page!" + } }; + +export default Messages; diff --git a/dashboard/src/utils/Utils.ts b/dashboard/src/utils/Utils.ts index dec30ec04..0e8b4fb95 100644 --- a/dashboard/src/utils/Utils.ts +++ b/dashboard/src/utils/Utils.ts @@ -22,7 +22,7 @@ import moment from "moment-timezone"; import sanitizeHtml from "sanitize-html"; import { cloneDeep, toArrayifObject, uniq } from "./Helper"; import { attributeFilter } from "./CommonViewFunction"; -import { Messages } from "./Messages"; +import Messages from "./Messages"; interface childrenInterface { gType: string; diff --git a/dashboard/src/views/Administrator/Audits/AdminAuditTable.tsx b/dashboard/src/views/Administrator/Audits/AdminAuditTable.tsx index 0a09cc3c7..04031e03f 100644 --- a/dashboard/src/views/Administrator/Audits/AdminAuditTable.tsx +++ b/dashboard/src/views/Administrator/Audits/AdminAuditTable.tsx @@ -52,14 +52,15 @@ const AdminAuditTable = () => { const fetchAuditResult = useCallback( async ({ pagination }: { pagination?: any }) => { const { pageSize, pageIndex } = pagination || {}; - if (pageIndex > 1) { - searchParams.set("pageOffset", `${pageSize * pageIndex}`); - } + // Derive strictly from table state to avoid URL race conditions + const limit = pageSize || 25; + const offset = (pageIndex || 0) * limit; + let params: any = { auditFilters: !isEmpty(queryApiObj) ? queryApiObj : null, - limit: pageSize, + limit: limit, sortOrder: "DESCENDING", - offset: pageIndex * pageSize, + offset: offset, sortBy: "startTime" }; diff --git a/dashboard/src/views/DetailPage/EntityDetailTabs/AuditsTab.tsx b/dashboard/src/views/DetailPage/EntityDetailTabs/AuditsTab.tsx index 79d8b0d40..4b4a1efa9 100644 --- a/dashboard/src/views/DetailPage/EntityDetailTabs/AuditsTab.tsx +++ b/dashboard/src/views/DetailPage/EntityDetailTabs/AuditsTab.tsx @@ -49,16 +49,16 @@ const AuditsTab = ({ sorting: [{ id: string; desc: boolean }]; }) => { const { pageSize, pageIndex } = pagination || {}; - if (pageIndex > 1) { - searchParams.set("pageOffset", `${pageSize * pageIndex}`); - } + const limitParam = searchParams.get("pageLimit"); + const offsetParam = searchParams.get("pageOffset"); + const limit = !isEmpty(limitParam) ? Number(limitParam) : pageSize; + const offset = !isEmpty(offsetParam) + ? Number(offsetParam) + : pageIndex * pageSize; let params: any = { sortOrder: sorting[0]?.desc == false ? "asc" : "desc", - offset: - searchParams.get("pageOffset") != null - ? Number(searchParams.get("pageOffset")) - : pageIndex * pageSize, - count: pageSize, + offset: offset, + limit: limit, sortBy: sorting[0]?.id || "timestamp" }; diff --git a/dashboard/src/views/DetailPage/EntityDetailTabs/ClassificationsTab.tsx b/dashboard/src/views/DetailPage/EntityDetailTabs/ClassificationsTab.tsx index adb162102..bba595dec 100644 --- a/dashboard/src/views/DetailPage/EntityDetailTabs/ClassificationsTab.tsx +++ b/dashboard/src/views/DetailPage/EntityDetailTabs/ClassificationsTab.tsx @@ -365,6 +365,9 @@ const ClassificationsTab: React.FC<EntityDetailTabProps> = ({ showPagination={true} showRowSelection={false} tableFilters={false} + showGoToPage={true} + setUpdateTable={setUpdateTable} + isClientSidePagination={true} /> </Stack> </Grid> diff --git a/dashboard/src/views/DetailPage/EntityDetailTabs/ProfileTab.tsx b/dashboard/src/views/DetailPage/EntityDetailTabs/ProfileTab.tsx index 81b8d001d..baea3e3c9 100644 --- a/dashboard/src/views/DetailPage/EntityDetailTabs/ProfileTab.tsx +++ b/dashboard/src/views/DetailPage/EntityDetailTabs/ProfileTab.tsx @@ -88,17 +88,15 @@ const ProfileTab: React.FC<EntityDetailTabProps> = ({ entity }) => { return; } const { pageSize, pageIndex } = pagination || {}; + const offsetParam = searchParams.get("pageOffset"); + const limitParam = searchParams.get("pageLimit"); if (pageIndex > 1) { - searchParams.set("pageOffset", `${pageSize * pageIndex}`); + searchParams.set("pageOffset", `${pageSize + pageIndex}`); } let params: any = { order: sorting[0]?.desc == false ? "asc" : "desc", - offset: - searchParams.get("pageOffset") !== undefined && - searchParams.get("pageOffset") !== null - ? Number(searchParams.get("pageOffset")) - : pageIndex * pageSize, - limit: pageSize, + offset: !isEmpty(offsetParam) ? offsetParam : pageIndex + pageSize, + limit: !isEmpty(limitParam) ? limitParam : pageSize, sort_by: sorting[0]?.id || "timestamp", guid: guid, relation: diff --git a/dashboard/src/views/DetailPage/EntityDetailTabs/ReplicationAuditTab.tsx b/dashboard/src/views/DetailPage/EntityDetailTabs/ReplicationAuditTab.tsx index 63ebb363d..6373f77e0 100644 --- a/dashboard/src/views/DetailPage/EntityDetailTabs/ReplicationAuditTab.tsx +++ b/dashboard/src/views/DetailPage/EntityDetailTabs/ReplicationAuditTab.tsx @@ -32,6 +32,7 @@ import RauditsTableResults from "./RauditsTableResults"; const ReplicationAuditTable = (props: any) => { const { entity = {}, referredEntities = {}, loading } = props; const [searchParams] = useSearchParams(); + const limitParam = searchParams.get("pageLimit"); const { guid } = useParams(); const toastId: any = useRef(null); const [loader, setLoader] = useState<boolean>(false); @@ -52,7 +53,7 @@ const ReplicationAuditTable = (props: any) => { let params: any = { serverName: name, - limit: pageSize + limit: !isEmpty(limitParam) ? limitParam : pageSize }; setLoader(true); diff --git a/dashboard/src/views/SearchResult/RelationShipSearch.tsx b/dashboard/src/views/SearchResult/RelationShipSearch.tsx index 6774f16b4..3b5e8e9ba 100644 --- a/dashboard/src/views/SearchResult/RelationShipSearch.tsx +++ b/dashboard/src/views/SearchResult/RelationShipSearch.tsx @@ -79,7 +79,7 @@ const RelationShipSearch: React.FC = () => { async ({ pagination }: { pagination?: any }) => { const { pageSize, pageIndex } = pagination || {}; if (pageIndex > 1) { - searchParams.set("pageOffset", `${pageSize * pageIndex}`); + searchParams.set("pageOffset", `${pageSize + pageIndex}`); } let params: Params = { attributes: !isEmpty(attributesParams) @@ -90,8 +90,8 @@ const RelationShipSearch: React.FC = () => { : [], limit: !isEmpty(limitParams) ? Number(limitParams) : pageSize, offset: !isEmpty(offsetParams) - ? Number(searchParams.get("pageOffset")) - : pageIndex * pageSize, + ? Number(offsetParams) + : pageIndex + pageSize, relationshipFilters: !isEmpty(relationshipFiltersParams) ? relationshipFiltersParams : null, diff --git a/dashboard/src/views/SearchResult/SearchResult.tsx b/dashboard/src/views/SearchResult/SearchResult.tsx index 90f0e55f4..aa1cd0921 100644 --- a/dashboard/src/views/SearchResult/SearchResult.tsx +++ b/dashboard/src/views/SearchResult/SearchResult.tsx @@ -91,6 +91,8 @@ const SearchResult = ({ classificationParams, glossaryTypeParams }: any) => { const [updateTable, setUpdateTable] = useState(moment.now()); const { entityData } = useSelector((state: EntityState) => state.entity); const [pageCount, setPageCount] = useState<number>(0); + const [totalCount, setTotalCount] = useState<number>(0); + const [isEmptyData, setIsEmptyData] = useState(false); const [checkedEntities, setCheckedEntities] = useState<any>( !isEmpty(searchParams.get("includeDE")) ? searchParams.get("includeDE") @@ -139,9 +141,7 @@ const SearchResult = ({ classificationParams, glossaryTypeParams }: any) => { async ({ pagination }: { pagination?: any }) => { setLoader(true); const { pageSize, pageIndex } = pagination || {}; - if (pageIndex > 1) { - searchParams.set("pageOffset", `${pageSize * pageIndex}`); - } + let params: Params | any = { excludeDeletedEntities: !isEmpty(searchParams.get("includeDE")) ? !searchParams.get("includeDE") @@ -210,7 +210,7 @@ const SearchResult = ({ classificationParams, glossaryTypeParams }: any) => { let searchTypeParams = searchParams.get("searchType") == "dsl" ? dslParams : params; try { - let searchResp = await getBasicSearchResult( + const searchResp = await getBasicSearchResult( { data: (searchParams.get("searchType") == "basic" || @@ -220,12 +220,29 @@ const SearchResult = ({ classificationParams, glossaryTypeParams }: any) => { }, searchParams.get("searchType") || "basic" ); - let totalCount = searchResp.data.approximateCount; - setSearchData(searchResp.data); - setPageCount(Math.ceil(totalCount / pagination.pageSize)); - setLoader(false); + const { data = {} } = searchResp || {}; + const { approximateCount, entities } = data || {}; + let totalCount = approximateCount; + let dataLength; + if (entities) { + dataLength = entities?.length; + } else { + dataLength = searchResp?.data?.length; + } + if (!dataLength) { + setIsEmptyData(true); + setLoader(false); + } else { + setSearchData(searchResp.data); + setTotalCount(totalCount || 0); + setPageCount(Math.ceil(totalCount / pagination.pageSize)); + setLoader(false); + } } catch (error: any) { - console.error("Error fetching data:", error.response.data.errorMessage); + console.error( + "Error fetching data:", + error?.response?.data?.errorMessage + ); toast.dismiss(toastId.current); serverError(error, toastId); setLoader(false); @@ -451,7 +468,6 @@ const SearchResult = ({ classificationParams, glossaryTypeParams }: any) => { obj.name === superTypesEntityData.name ); if (referredEntities == -1) { - // let referredEntities = searchData.referredEntities[obj?.guid]; superTypesObj = { accessorFn: (row: any) => row.attributes[superTypesEntityData.name], @@ -765,17 +781,7 @@ const SearchResult = ({ classificationParams, glossaryTypeParams }: any) => { const getDefaultSort = useMemo(() => [{ id: "name", asc: true }], []); return ( - <Stack - // paddingTop={ - // isEmpty(classificationParams || glossaryTypeParams) ? 0 : "40px" - // } - // marginTop={ - // isEmpty(classificationParams || glossaryTypeParams) ? 0 : "20px" - // } - // padding="16px" - position="relative" - gap={"1rem"} - > + <Stack position="relative" gap={"1rem"}> {!isEmpty(classificationParams || glossaryTypeParams) && ( <Stack direction="row" @@ -825,9 +831,6 @@ const SearchResult = ({ classificationParams, glossaryTypeParams }: any) => { </Stack> )} - {/* {loader ? ( - <CircularProgress /> - ) : ( */} <TableLayout fetchData={fetchSearchResult} data={searchData.entities || []} @@ -863,11 +866,13 @@ const SearchResult = ({ classificationParams, glossaryTypeParams }: any) => { allTableFilters={true} setUpdateTable={setUpdateTable} isfilterQuery={true} + isEmptyData={isEmptyData} + setIsEmptyData={setIsEmptyData} + showGoToPage={true} + totalCount={totalCount} /> - {/* )} */} </Stack> ); - // ); }; export default SearchResult; diff --git a/dashboard/src/views/SideBar/SideBarTree/SideBarTree.tsx b/dashboard/src/views/SideBar/SideBarTree/SideBarTree.tsx index 1d3542cf3..d4980a848 100644 --- a/dashboard/src/views/SideBar/SideBarTree/SideBarTree.tsx +++ b/dashboard/src/views/SideBar/SideBarTree/SideBarTree.tsx @@ -602,7 +602,11 @@ const BarTreeView: FC<{ searchParams.delete("entityFilters"); searchParams.delete("tagFilters"); searchParams.delete("relationshipFilters"); - searchParams.delete("pageOffset"); + // Always reset pagination defaults on tree navigation + if (treeName !== "CustomFilters") { + searchParams.set("pageLimit", "25"); + searchParams.set("pageOffset", "0"); + } }; const setGlossarySearchParams = (
