From 3a154e8056d2a8afa511c82675117981ba58a3e3 Mon Sep 17 00:00:00 2001 From: Josh Date: Tue, 15 Dec 2020 20:19:50 -0800 Subject: [PATCH] explorer: Token history dropdown filter (#14032) * feat: add filter on token history card * rename to filter * use unicode ellipsis * better naming * filter options read better and use just pubkey strings * memoize filtered list * only fetch filtered * pre filter tokens * fix prettier --- .../components/account/TokenHistoryCard.tsx | 145 ++++++++++++++++-- explorer/src/scss/_solana.scss | 5 + 2 files changed, 137 insertions(+), 13 deletions(-) diff --git a/explorer/src/components/account/TokenHistoryCard.tsx b/explorer/src/components/account/TokenHistoryCard.tsx index 7c3e937643..19a4673aca 100644 --- a/explorer/src/components/account/TokenHistoryCard.tsx +++ b/explorer/src/components/account/TokenHistoryCard.tsx @@ -32,7 +32,7 @@ import { IX_TITLES, } from "components/instruction/token/types"; import { reportError } from "utils/sentry"; -import { intoTransactionInstruction } from "utils/tx"; +import { intoTransactionInstruction, displayAddress } from "utils/tx"; import { isTokenSwapInstruction, parseTokenSwapInstructionTitle, @@ -43,6 +43,12 @@ import { } from "components/instruction/serum/types"; import { INNER_INSTRUCTIONS_START_SLOT } from "pages/TransactionDetailsPage"; import { useCluster, Cluster } from "providers/cluster"; +import { Link } from "react-router-dom"; +import { Location } from "history"; +import { useQuery } from "utils/url"; + +const TRUNCATE_TOKEN_LENGTH = 10; +const ALL_TOKENS = ""; type InstructionType = { name: string; @@ -69,20 +75,49 @@ export function TokenHistoryCard({ pubkey }: { pubkey: PublicKey }) { return ; } +const useQueryFilter = (): string => { + const query = useQuery(); + const filter = query.get("filter"); + return filter || ""; +}; + +type FilterProps = { + filter: string; + toggle: () => void; + show: boolean; + tokens: TokenInfoWithPubkey[]; +}; + function TokenHistoryTable({ tokens }: { tokens: TokenInfoWithPubkey[] }) { const accountHistories = useAccountHistories(); const fetchAccountHistory = useFetchAccountHistory(); const transactionDetailsCache = useTransactionDetailsCache(); + const [showDropdown, setDropdown] = React.useState(false); + const filter = useQueryFilter(); - const fetchHistories = (refresh?: boolean) => { - tokens.forEach((token) => { - fetchAccountHistory(token.pubkey, refresh); - }); - }; + const filteredTokens = React.useMemo( + () => + tokens.filter((token) => { + if (filter === ALL_TOKENS) { + return true; + } + return token.info.mint.toBase58() === filter; + }), + [tokens, filter] + ); + + const fetchHistories = React.useCallback( + (refresh?: boolean) => { + filteredTokens.forEach((token) => { + fetchAccountHistory(token.pubkey, refresh); + }); + }, + [filteredTokens, fetchAccountHistory] + ); // Fetch histories on load React.useEffect(() => { - tokens.forEach((token) => { + filteredTokens.forEach((token) => { const address = token.pubkey.toBase58(); if (!accountHistories[address]) { fetchAccountHistory(token.pubkey, true); @@ -90,20 +125,21 @@ function TokenHistoryTable({ tokens }: { tokens: TokenInfoWithPubkey[] }) { }); }, []); // eslint-disable-line react-hooks/exhaustive-deps - const allFoundOldest = tokens.every((token) => { + const allFoundOldest = filteredTokens.every((token) => { const history = accountHistories[token.pubkey.toBase58()]; return history?.data?.foundOldest === true; }); - const allFetchedSome = tokens.every((token) => { + const allFetchedSome = filteredTokens.every((token) => { const history = accountHistories[token.pubkey.toBase58()]; return history?.data !== undefined; }); // Find the oldest slot which we know we have the full history for let oldestSlot: number | undefined = allFoundOldest ? 0 : undefined; + if (!allFoundOldest && allFetchedSome) { - tokens.forEach((token) => { + filteredTokens.forEach((token) => { const history = accountHistories[token.pubkey.toBase58()]; if (history?.data?.foundOldest === false) { const earliest = @@ -114,18 +150,18 @@ function TokenHistoryTable({ tokens }: { tokens: TokenInfoWithPubkey[] }) { }); } - const fetching = tokens.some((token) => { + const fetching = filteredTokens.some((token) => { const history = accountHistories[token.pubkey.toBase58()]; return history?.status === FetchStatus.Fetching; }); - const failed = tokens.some((token) => { + const failed = filteredTokens.some((token) => { const history = accountHistories[token.pubkey.toBase58()]; return history?.status === FetchStatus.FetchFailed; }); const sigSet = new Set(); - const mintAndTxs = tokens + const mintAndTxs = filteredTokens .map((token) => ({ mint: token.info.mint, history: accountHistories[token.pubkey.toBase58()], @@ -148,6 +184,12 @@ function TokenHistoryTable({ tokens }: { tokens: TokenInfoWithPubkey[] }) { return oldestSlot !== undefined && tx.slot >= oldestSlot; }); + React.useEffect(() => { + if (!fetching && mintAndTxs.length < 1 && !allFoundOldest) { + fetchHistories(); + } + }, [fetching, mintAndTxs, allFoundOldest, fetchHistories]); + if (mintAndTxs.length === 0) { if (fetching) { return ; @@ -178,6 +220,12 @@ function TokenHistoryTable({ tokens }: { tokens: TokenInfoWithPubkey[] }) {

Token History

+ setDropdown((show) => !show)} + show={showDropdown} + tokens={tokens} + > +
+ {filterOptions.map((filterOption) => { + return ( + buildLocation(location, filterOption)} + className={`dropdown-item${ + filterOption === filter ? " active" : "" + }`} + onClick={toggle} + > + {filterOption === ALL_TOKENS + ? "All Tokens" + : formatTokenName(filterOption, cluster)} + + ); + })} +
+
+ ); +}; + function instructionTypeName( ix: ParsedInstruction, tx: ConfirmedSignatureInfo @@ -478,3 +587,13 @@ function InstructionDetails({ ); } + +function formatTokenName(pubkey: string, cluster: Cluster): string { + let display = displayAddress(pubkey, cluster); + + if (display === pubkey) { + display = display.slice(0, TRUNCATE_TOKEN_LENGTH) + "\u2026"; + } + + return display; +} diff --git a/explorer/src/scss/_solana.scss b/explorer/src/scss/_solana.scss index 9183168f16..f70f0c5a15 100644 --- a/explorer/src/scss/_solana.scss +++ b/explorer/src/scss/_solana.scss @@ -321,3 +321,8 @@ div.inner-cards { content:''; } } + +.dropdown-menu.token-filter { + max-height: 20rem; + overflow-y: auto; +}