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
This commit is contained in:
Josh 2020-12-15 20:19:50 -08:00 committed by GitHub
parent db4ac17259
commit 3a154e8056
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
2 changed files with 137 additions and 13 deletions

View File

@ -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 <TokenHistoryTable tokens={tokens} />;
}
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 <LoadingCard message="Loading history" />;
@ -178,6 +220,12 @@ function TokenHistoryTable({ tokens }: { tokens: TokenInfoWithPubkey[] }) {
<div className="card">
<div className="card-header align-items-center">
<h3 className="card-header-title">Token History</h3>
<FilterDropdown
filter={filter}
toggle={() => setDropdown((show) => !show)}
show={showDropdown}
tokens={tokens}
></FilterDropdown>
<button
className="btn btn-white btn-sm"
disabled={fetching}
@ -245,6 +293,67 @@ function TokenHistoryTable({ tokens }: { tokens: TokenInfoWithPubkey[] }) {
);
}
const FilterDropdown = ({ filter, toggle, show, tokens }: FilterProps) => {
const { cluster } = useCluster();
const buildLocation = (location: Location, filter: string) => {
const params = new URLSearchParams(location.search);
if (filter === ALL_TOKENS) {
params.delete("filter");
} else {
params.set("filter", filter);
}
return {
...location,
search: params.toString(),
};
};
const filterOptions: string[] = [ALL_TOKENS];
const nameLookup: { [mint: string]: string } = {};
tokens.forEach((token) => {
const pubkey = token.info.mint.toBase58();
filterOptions.push(pubkey);
nameLookup[pubkey] = formatTokenName(pubkey, cluster);
});
return (
<div className="dropdown mr-2">
<small className="mr-2">Filter:</small>
<button
className="btn btn-white btn-sm dropdown-toggle"
type="button"
onClick={toggle}
>
{filter === ALL_TOKENS ? "All Tokens" : nameLookup[filter]}
</button>
<div
className={`token-filter dropdown-menu-right dropdown-menu${
show ? " show" : ""
}`}
>
{filterOptions.map((filterOption) => {
return (
<Link
key={filterOption}
to={(location: Location) => buildLocation(location, filterOption)}
className={`dropdown-item${
filterOption === filter ? " active" : ""
}`}
onClick={toggle}
>
{filterOption === ALL_TOKENS
? "All Tokens"
: formatTokenName(filterOption, cluster)}
</Link>
);
})}
</div>
</div>
);
};
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;
}

View File

@ -321,3 +321,8 @@ div.inner-cards {
content:'';
}
}
.dropdown-menu.token-filter {
max-height: 20rem;
overflow-y: auto;
}