Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Fix: Added breaks and limits for pagination on resource lists #17

Merged
merged 2 commits into from
Aug 11, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -77,15 +77,15 @@ function VariablesTable(props: IVariablesTableProps) {
const [selectedVariables, setSelectedVariables] = useState<any>({});
const [hasLocation, setHasLocation] = useState(false);
const [hasMetadata, setHasMetadata] = useState(false);
const [page, setPage] = useState(0);
const [page, setPage] = useState(1);
const match = useRouteMatch<{ id: string }>();
const { id } = match.params;

/**
* Called by the table to retrieve data for a page.
*/
const onGetData = async (_page: number, amount: number, filter: IFilter) => {
const result = await getDeviceData(id, page, amount, filter, currentEndDate);
const result = await getDeviceData(id, page - 1, amount, filter, currentEndDate);
setAmountOfRecords(result.length);
setHasLocation(result.some((x) => x.location));
setHasMetadata(result.some((x) => x.metadata));
Expand All @@ -104,10 +104,10 @@ function VariablesTable(props: IVariablesTableProps) {
const refresh = useCallback(() => {
onReloadDataAmount();
setCurrentEndDate(new Date().toISOString());
if (page === 0) {
if (page === 1) {
onReloadTable();
} else {
setPage(0);
setPage(1);
}
}, [onReloadDataAmount, onReloadTable, page]);

Expand Down
4 changes: 2 additions & 2 deletions packages/tcore-console/src/Components/ListPage/ListPage.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -58,7 +58,7 @@ interface IListPageProps<T> {
* List page for a certain type of resource.
*/
function ListPage<T extends { id?: string }>(props: IListPageProps<T>) {
const [page, setPage] = useState(0);
const [page, setPage] = useState(1);
const [infinitePages, setInfinitePages] = useState(false);
const [amountOfRecords, setAmountOfRecords] = useState(0);
const tagValues = useRef<ITag[]>([]);
Expand Down Expand Up @@ -89,7 +89,7 @@ function ListPage<T extends { id?: string }>(props: IListPageProps<T>) {
const usingFilter = Object.keys(filterWithTags).some(
(x) => filterWithTags[x] !== undefined && filterWithTags[x] !== ""
);
const data = await onGetData(pg + 1, idealAmountOfRows, filterWithTags);
const data = await onGetData(pg, idealAmountOfRows, filterWithTags);

setAmountOfRecords(data.length);
setInfinitePages(usingFilter);
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,93 @@
// Number of page buttons to display in narrow containers
const MIN_PAGES = 3;
// Number of page buttons to display in medium containers
const MID_PAGES = 5;
// Number of page buttons to display in wide containers
const MAX_PAGES = 7;

// Minimum width of the page buttons
const BUTTON_WIDTH = 30;
// Padding around the pagination buttons to give space around the edges
const PAGINATION_PADDING = 30;

/**
* Get the amount of pages fitting in a container according to its width.
*
* @param paginationWidth Width of the container from the ref.
*/
const getPageAmount = (paginationWidth: number): number => {
// Return max when ref is still undefined as most tables should be wide enough
if (!paginationWidth) {
return MAX_PAGES;
}

const usedWidth = paginationWidth;
const availableWidth = usedWidth - PAGINATION_PADDING - 2 * BUTTON_WIDTH;
const maxPageAmount = Math.floor(availableWidth / BUTTON_WIDTH);
const pageAmount = Math.max(Math.min(maxPageAmount, MAX_PAGES), MIN_PAGES);

if (pageAmount === MAX_PAGES || pageAmount === MIN_PAGES) {
return pageAmount;
} else {
return MID_PAGES;
}
};

/**
* Gets the array of page numbers and the separators according to the range
* being displayed.
*
* @param amountOfPages Amount of pages.
* @param paginationWidth Width of the container from the ref.
* @param page Current page.
*/
const getPageList = (
amountOfPages: number,
paginationWidth: number,
page: number
): Array<"..." | number> => {
// we need to round them out because floating numbers can break the Array() call:
let amountOfPagesRounded = Math.round(amountOfPages) || 1;

if (isNaN(amountOfPagesRounded) || amountOfPagesRounded === Infinity) {
// extreme cases we will use at least one page
amountOfPagesRounded = 1;
}

const pageNumbers = Array.from(Array(amountOfPagesRounded).keys()).map((pageIdx) => pageIdx + 1);

const maxPages = getPageAmount(paginationWidth);

// Get the pages with ellipsis separating the current page from the boundary
// pages and sibling pages only amount of pages is bigger than space available
if (amountOfPagesRounded > maxPages) {
const lowerBound = maxPages - 2;
const upperBound = amountOfPagesRounded - (maxPages - 3);

const pageInLowerBound = page <= lowerBound;
const pageInUpperBound = page >= upperBound;

// Sibling pages are the page buttons around the current page
const siblings = maxPages === MAX_PAGES ? 1 : 0;

if (pageInLowerBound) {
return [...pageNumbers.slice(0, lowerBound), "...", amountOfPagesRounded];
} else if (pageInUpperBound) {
return [1, "...", ...pageNumbers.slice(upperBound - 1, amountOfPagesRounded)];
} else if (maxPages === MIN_PAGES) {
return ["...", page, "..."];
} else {
return [
1,
"...",
...pageNumbers.slice(page - (1 + siblings), page + siblings),
"...",
amountOfPagesRounded,
];
}
}

return pageNumbers;
};

export { MIN_PAGES, MID_PAGES, MAX_PAGES, getPageAmount, getPageList };
Original file line number Diff line number Diff line change
Expand Up @@ -85,3 +85,14 @@ export const Button = styled.button<{ selected?: boolean }>`
background: rgba(0, 0, 0, 0.25);
`}
`;

export const PaginationSeparator = styled.div`
border: 1px solid transparent;
padding: 0;
border-radius: 5px;
min-width: 30px;
width: initial;
display: flex;
align-items: center;
justify-content: center;
`;
51 changes: 37 additions & 14 deletions packages/tcore-console/src/Components/Pagination/Pagination.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
import { useRef } from "react";
import { Tooltip } from "../..";
import Icon from "../Icon/Icon";
import { EIcon } from "../Icon/Icon.types";
import { getPageList } from "./Pagination.logic";
import * as Style from "./Pagination.style";

/**
Expand Down Expand Up @@ -51,7 +53,7 @@ interface IPaginationProps {
*/
function Pagination(props: IPaginationProps) {
const { page, pageAmount, amountOfRecords, idealAmountOfRows, infinitePages, onChange } = props;
const array = new Array(pageAmount || 1).fill("").map((_, i) => i);
const containerRef = useRef<HTMLDivElement>(null);

/**
* Goes back one page.
Expand All @@ -70,13 +72,17 @@ function Pagination(props: IPaginationProps) {
/**
* Renders a single item.
*/
const renderItem = (value: number) => {
const renderItem = (value: number | string, index: number) => {
const selected = page === value;
return (
<Style.Button key={value} onClick={() => onChange(value)} selected={selected}>
{value + 1}
</Style.Button>
);
if (typeof value === "string") {
return <Style.PaginationSeparator key={`separator-${index}`}>...</Style.PaginationSeparator>;
} else {
return (
<Style.Button key={value} onClick={() => onChange(value)} selected={selected}>
{value}
</Style.Button>
);
}
};

/**
Expand All @@ -89,8 +95,29 @@ function Pagination(props: IPaginationProps) {
);
};

/**
* Renders the 'finite' pages in this component. Finite pages are the ones
* that have a start and end, different from the infinite pages which
* just show two arrows.
*/
const renderFinitePages = () => {
if (!containerRef?.current?.clientWidth) {
return null;
}

const array = getPageList(pageAmount, containerRef?.current?.clientWidth, page);
const nextDisabled = !pageAmount || page === pageAmount;
return (
<>
{renderArrow(EIcon["chevron-left"], page === 1, goBack)}
{array.map(renderItem)}
{renderArrow(EIcon["chevron-right"], nextDisabled, goForward)}
</>
);
};

return (
<Style.Container>
<Style.Container ref={containerRef}>
{props.showConfigButton && (
<Tooltip text="Configure table">
<div className="config-button" onClick={props.onConfigButtonClick}>
Expand All @@ -101,19 +128,15 @@ function Pagination(props: IPaginationProps) {

{infinitePages ? (
<>
{renderArrow(EIcon["chevron-left"], page === 0, goBack)}
{renderArrow(EIcon["chevron-left"], page === 1, goBack)}
{renderArrow(
EIcon["chevron-right"],
(amountOfRecords || 0) < (idealAmountOfRows || 0),
goForward
)}
</>
) : pageAmount > 0 ? (
<>
{renderArrow(EIcon["chevron-left"], page === 0, goBack)}
{array.map(renderItem)}
{renderArrow(EIcon["chevron-right"], page === pageAmount - 1, goForward)}
</>
renderFinitePages()
) : null}

{props.showRefreshButton && (
Expand Down