Skip to content

Commit

Permalink
feat: add pagination to proposals table (#855)
Browse files Browse the repository at this point in the history
Also rename ValidatorsTable to TableWithPaginator and refactor to allow
use with things other than validators.

Closes #843.
  • Loading branch information
emccorson committed Jun 10, 2024
1 parent 3ad6200 commit b792ea1
Show file tree
Hide file tree
Showing 3 changed files with 156 additions and 141 deletions.
100 changes: 100 additions & 0 deletions apps/namadillo/src/App/Common/TableWithPaginator.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,100 @@
import { StyledTable, TableHeader, TableRow } from "@namada/components";
import { FormattedPaginator } from "App/Common/FormattedPaginator";
import clsx from "clsx";
import { useCallback, useEffect, useMemo, useRef, useState } from "react";

type TableWithPaginatorProps<T> = {
id: string;
headers: (TableHeader | React.ReactNode)[];
itemList: T[];
renderRow: (item: T, index: number) => TableRow;
resultsPerPage?: number;
initialPage?: number;
children?: React.ReactNode;
} & Pick<React.ComponentProps<typeof StyledTable>, "tableProps" | "headProps">;

export const TableWithPaginator = <T,>({
id,
headers,
renderRow,
itemList,
resultsPerPage = 100,
initialPage = 0,
tableProps,
headProps,
children,
}: TableWithPaginatorProps<T>): JSX.Element => {
const containerRef = useRef<HTMLDivElement>(null);
const [page, setPage] = useState(initialPage);
const [rows, setRows] = useState<TableRow[]>([]);

const paginatedItems = itemList.slice(
page * resultsPerPage,
page * resultsPerPage + resultsPerPage
);

const pageCount = Math.ceil(itemList.length / resultsPerPage);

useEffect(() => {
setPage(0);
}, [itemList]);

// Update all rows
useEffect(() => {
setRows(paginatedItems.map(renderRow));
}, [renderRow, page, itemList]);

const scrollTop = useCallback((): void => {
const container = containerRef.current!.querySelector(".table-container");
if (container) {
container.scrollTo({ top: 0, left: 0 });
}
}, []);

const styledTable = useMemo(() => {
return (
<StyledTable
id={id}
headers={headers}
rows={rows}
containerClassName="table-container flex-1 dark-scrollbar overscroll-contain"
tableProps={tableProps}
headProps={headProps}
/>
);
}, [rows, tableProps, headProps]);

const pagination = useMemo(() => {
return (
<FormattedPaginator
pageRangeDisplayed={3}
pageCount={pageCount}
onPageChange={({ selected }) => {
setPage(selected);
scrollTop();
}}
/>
);
}, [page, itemList]);

if (rows.length === 0) {
return (
<div className="flex items-center justify-center text-sm py-4 text-neutral-200">
No results were found
</div>
);
}

return (
<div
ref={containerRef}
className={clsx(
"grid grid-rows-[auto_max-content] flex-1 overflow-hidden w-full gap-2"
)}
>
{styledTable}
{pagination}
{children}
</div>
);
};
35 changes: 15 additions & 20 deletions apps/namadillo/src/App/Governance/AllProposalsTable.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,14 +3,10 @@ import { useState } from "react";
import { GoCheckCircleFill, GoInfo } from "react-icons/go";
import { useNavigate } from "react-router-dom";

import {
Stack,
StyledSelectBox,
StyledTable,
TableRow,
} from "@namada/components";
import { Stack, StyledSelectBox, TableRow } from "@namada/components";
import { Proposal, isProposalStatus, proposalStatuses } from "@namada/types";
import { Search } from "App/Common/Search";
import { TableWithPaginator } from "App/Common/TableWithPaginator";
import clsx from "clsx";
import { allProposalsFamily, proposalFamily } from "slices/proposals";
import { showProposalStatus, showProposalTypeString } from "utils";
Expand Down Expand Up @@ -77,16 +73,15 @@ const Table: React.FC<
});

return (
<div className="h-[490px] flex flex-col">
<StyledTable
tableProps={{ className: "w-full text-xs [&_td]:px-2 [&_th]:px-2" }}
headProps={{ className: "text-xs" }}
id="all-proposals-table"
headers={headers}
rows={props.proposalIds.map(renderRow)}
containerClassName="dark-scrollbar"
/>
</div>
<TableWithPaginator
id="all-proposals-table"
headers={headers}
itemList={props.proposalIds}
renderRow={renderRow}
tableProps={{ className: "w-full text-xs [&_td]:px-2 [&_th]:px-2" }}
headProps={{ className: "text-xs" }}
resultsPerPage={50}
/>
);
};

Expand Down Expand Up @@ -206,10 +201,10 @@ export const AllProposalsTable: React.FC<ExtensionConnectedProps> = (props) => {
/>
</div>

<Table
{...props}
proposalIds={proposals.isSuccess ? proposals.data.map((p) => p.id) : []}
/>
<div className="h-[490px] flex flex-col">
{proposals.status === "error" || proposals.status === "pending" ? null
: <Table {...props} proposalIds={proposals.data.map((p) => p.id)} />}
</div>
</Stack>
);
};
Expand Down
162 changes: 41 additions & 121 deletions apps/namadillo/src/App/Staking/ValidatorsTable.tsx
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
import { StyledTable, TableHeader, TableRow } from "@namada/components";
import { FormattedPaginator } from "App/Common/FormattedPaginator";
import { TableHeader, TableRow } from "@namada/components";
import { TableWithPaginator } from "App/Common/TableWithPaginator";
import BigNumber from "bignumber.js";
import clsx from "clsx";
import { useCallback, useEffect, useMemo, useRef, useState } from "react";
import { useCallback, useEffect, useState } from "react";
import { GoInfo } from "react-icons/go";
import { Validator } from "slices/validators";
import { twMerge } from "tailwind-merge";
Expand All @@ -27,30 +27,11 @@ export const ValidatorsTable = ({
resultsPerPage = 100,
initialPage = 0,
tableClassName,
updatedAmountByAddress,
}: ValidatorsTableProps): JSX.Element => {
const containerRef = useRef<HTMLDivElement>(null);
const [page, setPage] = useState(initialPage);
const [rows, setRows] = useState<TableRow[]>([]);
const [selectedValidator, setSelectedValidator] = useState<
Validator | undefined
>();

const updatesTracker = useRef<typeof updatedAmountByAddress>(
updatedAmountByAddress || {}
);

const paginatedValidators = validatorList.slice(
page * resultsPerPage,
page * resultsPerPage + resultsPerPage
);

const pageCount = Math.ceil(validatorList.length / resultsPerPage);

useEffect(() => {
setPage(0);
}, [validatorList]);

useEffect(() => {
const onCloseInfo = (): void => setSelectedValidator(undefined);
document.documentElement.addEventListener("click", onCloseInfo);
Expand All @@ -59,114 +40,53 @@ export const ValidatorsTable = ({
};
}, []);

// Update all validators
useEffect(() => {
setRows(paginatedValidators.map(mapValidatorRow));
}, [renderRow, page, validatorList]);

// Only updates the addresses contained in updatedAmountByAddress that have changed
useEffect(() => {
if (!updatedAmountByAddress) return;
setRows((rows) => {
const newRows = [...rows];
paginatedValidators.forEach((validator, idx) => {
const skipUpdate =
(!updatesTracker.current![validator.address] &&
!updatedAmountByAddress[validator.address]) ||
updatesTracker.current![validator.address] ===
updatedAmountByAddress[validator.address];

if (skipUpdate) return;
newRows[idx] = mapValidatorRow(validator);
});
updatesTracker.current = { ...updatedAmountByAddress };
return newRows;
});
}, [updatedAmountByAddress]);

const mapValidatorRow = (validator: Validator): TableRow => {
const row = renderRow(validator);
row.cells.push(
<i
onClick={(e) => {
e.stopPropagation();
setSelectedValidator(validator);
}}
className={clsx(
"cursor-pointer flex justify-end relative",
"hover:text-cyan active:top-px"
)}
>
<GoInfo />
</i>
);
return row;
};

const scrollTop = useCallback((): void => {
const container = containerRef.current!.querySelector(".table-container");
if (container) {
container.scrollTo({ top: 0, left: 0 });
}
}, []);

const styledTable = useMemo(() => {
return (
<StyledTable
id={id}
headers={headers.concat("")}
rows={rows}
containerClassName="table-container flex-1 dark-scrollbar overscroll-contain"
tableProps={{
className: twMerge(
"w-full flex-1 [&_td]:px-1 [&_th]:px-1 [&_td:first-child]:pl-4 [&_td]:h-[64px]",
"[&_td]:font-normal [&_td:last-child]:pr-4 [&_th:first-child]:pl-4 [&_th:last-child]:pr-4",
"[&_td:first-child]:rounded-s-md [&_td:last-child]:rounded-e-md",
tableClassName
),
}}
headProps={{ className: "text-neutral-500" }}
/>
);
}, [rows, updatedAmountByAddress, tableClassName]);

const pagination = useMemo(() => {
return (
<FormattedPaginator
pageRangeDisplayed={3}
pageCount={pageCount}
onPageChange={({ selected }) => {
setPage(selected);
scrollTop();
}}
/>
);
}, [page, validatorList]);

if (rows.length === 0) {
return (
<div className="flex items-center justify-center text-sm py-4 text-neutral-200">
No results were found
</div>
);
}
const mapValidatorRow = useCallback(
(validator: Validator): TableRow => {
const row = renderRow(validator);
row.cells.push(
<i
onClick={(e) => {
e.stopPropagation();
setSelectedValidator(validator);
}}
className={clsx(
"cursor-pointer flex justify-end relative",
"hover:text-cyan active:top-px"
)}
>
<GoInfo />
</i>
);
return row;
},
[renderRow, setSelectedValidator]
);

return (
<div
ref={containerRef}
className={clsx(
"grid grid-rows-[auto_max-content] flex-1 overflow-hidden w-full gap-2"
)}
<TableWithPaginator
id={id}
headers={headers.concat("")}
renderRow={mapValidatorRow}
itemList={validatorList}
resultsPerPage={resultsPerPage}
initialPage={initialPage}
tableProps={{
className: twMerge(
"w-full flex-1 [&_td]:px-1 [&_th]:px-1 [&_td:first-child]:pl-4 [&_td]:h-[64px]",
"[&_td]:font-normal [&_td:last-child]:pr-4 [&_th:first-child]:pl-4 [&_th:last-child]:pr-4",
"[&_td:first-child]:rounded-s-md [&_td:last-child]:rounded-e-md",
tableClassName
),
}}
headProps={{ className: "text-neutral-500" }}
>
{styledTable}
{pagination}
{selectedValidator && (
<ValidatorInfoPanel
className="h-full right-0 top-0"
validator={selectedValidator}
onClose={() => setSelectedValidator(undefined)}
/>
)}
</div>
</TableWithPaginator>
);
};

0 comments on commit b792ea1

Please sign in to comment.