Move top accounts to supply page and add filter

This commit is contained in:
Justin Starry 2020-06-02 17:27:07 +08:00 committed by Michael Vines
parent 16b0ee0a21
commit 6c45729694
4 changed files with 181 additions and 49 deletions

View File

@ -34,13 +34,16 @@ function App() {
<Route exact path="/supply">
<TabbedPage tab="Supply">
<SupplyCard />
</TabbedPage>
</Route>
<Route exact path="/accounts/top">
<TabbedPage tab="Accounts">
<TopAccountsCard />
</TabbedPage>
</Route>
<Route
exact
path="/accounts/top"
render={({ location }) => (
<Redirect to={{ ...location, pathname: "/supply" }} />
)}
></Route>
<Route
exact
path={TX_ALIASES.flatMap(tx => [tx, tx + "s"]).map(

View File

@ -39,6 +39,11 @@ export default function SupplyCard() {
<td className="w-100">Circulating Supply (SOL)</td>
<td>{lamportsToSolString(supply.circulating)}</td>
</tr>
<tr>
<td className="w-100">Non-Circulating Supply (SOL)</td>
<td>{lamportsToSolString(supply.nonCirculating)}</td>
</tr>
</TableCardBody>
</div>
);
@ -49,7 +54,7 @@ const renderHeader = () => {
<div className="card-header">
<div className="row align-items-center">
<div className="col">
<h4 className="card-header-title">Supply Stats</h4>
<h4 className="card-header-title">Overview</h4>
</div>
</div>
</div>

View File

@ -1,20 +1,30 @@
import React from "react";
import { Link } from "react-router-dom";
import { Location } from "history";
import { AccountBalancePair } from "@solana/web3.js";
import Copyable from "./Copyable";
import { useRichList, useFetchRichList, Status } from "providers/richList";
import LoadingCard from "./common/LoadingCard";
import ErrorCard from "./common/ErrorCard";
import { lamportsToSolString } from "utils";
import { useQuery } from "utils/url";
import { useSupply } from "providers/supply";
type Filter = "circulating" | "nonCirculating" | null;
export default function TopAccountsCard() {
const supply = useSupply();
const richList = useRichList();
const fetchRichList = useFetchRichList();
const [showDropdown, setDropdown] = React.useState(false);
const filter = useQueryFilter();
// Fetch on load
React.useEffect(() => {
if (richList === Status.Idle) fetchRichList();
}, []); // eslint-disable-line react-hooks/exhaustive-deps
if (richList === Status.Idle && typeof supply === "object") fetchRichList();
}, [supply]); // eslint-disable-line react-hooks/exhaustive-deps
if (typeof supply !== "object") return null;
if (richList === Status.Disconnected) {
return <ErrorCard text="Not connected to the cluster" />;
@ -27,11 +37,51 @@ export default function TopAccountsCard() {
return <ErrorCard text={richList} retry={fetchRichList} />;
}
const { accounts, totalSupply: supply } = richList;
let supplyCount: number;
let accounts, header;
switch (filter) {
case "circulating": {
accounts = richList.circulating;
supplyCount = supply.circulating;
header = "Circulating";
break;
}
case "nonCirculating": {
accounts = richList.nonCirculating;
supplyCount = supply.nonCirculating;
header = "Non-Circulating";
break;
}
default: {
accounts = richList.total;
supplyCount = supply.total;
header = "Total";
break;
}
}
return (
<>
{showDropdown && (
<div className="dropdown-exit" onClick={() => setDropdown(false)} />
)}
<div className="card">
{renderHeader()}
<div className="card-header">
<div className="row align-items-center">
<div className="col">
<h4 className="card-header-title">Largest Accounts</h4>
</div>
<div className="col-auto">
<FilterDropdown
filter={filter}
toggle={() => setDropdown(show => !show)}
show={showDropdown}
/>
</div>
</div>
</div>
<div className="table-responsive mb-0">
<table className="table table-sm table-nowrap card-table">
@ -40,28 +90,93 @@ export default function TopAccountsCard() {
<th className="text-muted">Rank</th>
<th className="text-muted">Address</th>
<th className="text-muted">Balance (SOL)</th>
<th className="text-muted">% of Total Supply</th>
<th className="text-muted">% of {header} Supply</th>
<th className="text-muted">Details</th>
</tr>
</thead>
<tbody className="list">
{accounts.map((account, index) =>
renderAccountRow(account, index, supply)
renderAccountRow(account, index, supplyCount)
)}
</tbody>
</table>
</div>
</div>
</>
);
}
const renderHeader = () => {
const useQueryFilter = (): Filter => {
const query = useQuery();
const filter = query.get("filter");
if (filter === "circulating" || filter === "nonCirculating") {
return filter;
} else {
return null;
}
};
const filterTitle = (filter: Filter): string => {
switch (filter) {
case "circulating": {
return "Circulating";
}
case "nonCirculating": {
return "Non-Circulating";
}
default: {
return "All";
}
}
};
type DropdownProps = {
filter: Filter;
toggle: () => void;
show: boolean;
};
const FilterDropdown = ({ filter, toggle, show }: DropdownProps) => {
const buildLocation = (location: Location, filter: Filter) => {
const params = new URLSearchParams(location.search);
if (filter === null) {
params.delete("filter");
} else {
params.set("filter", filter);
}
return {
...location,
search: params.toString()
};
};
const FILTERS: Filter[] = [null, "circulating", "nonCirculating"];
return (
<div className="card-header">
<div className="row align-items-center">
<div className="col">
<h4 className="card-header-title">Largest Accounts</h4>
</div>
<div className="dropdown">
<button
className="btn btn-white btn-sm dropdown-toggle"
type="button"
onClick={toggle}
>
{filterTitle(filter)}
</button>
<div
className={`dropdown-menu-right dropdown-menu${show ? " show" : ""}`}
>
{FILTERS.map(filterOption => {
return (
<Link
key={filterOption || "null"}
to={location => buildLocation(location, filterOption)}
className={`dropdown-item${
filterOption === filter ? " active" : ""
}`}
onClick={toggle}
>
{filterTitle(filterOption)}
</Link>
);
})}
</div>
</div>
);

View File

@ -9,13 +9,13 @@ export enum Status {
Connecting
}
type RichList = {
accounts: AccountBalancePair[];
totalSupply: number;
circulatingSupply: number;
type RichLists = {
total: AccountBalancePair[];
circulating: AccountBalancePair[];
nonCirculating: AccountBalancePair[];
};
type State = RichList | Status | string;
type State = RichLists | Status | string;
type Dispatch = React.Dispatch<React.SetStateAction<State>>;
const StateContext = React.createContext<State | undefined>(undefined);
@ -28,9 +28,16 @@ export function RichListProvider({ children }: Props) {
React.useEffect(() => {
if (state !== Status.Idle) {
if (clusterStatus === ClusterStatus.Connecting)
switch (clusterStatus) {
case ClusterStatus.Connecting: {
setState(Status.Disconnected);
if (clusterStatus === ClusterStatus.Connected) fetch(setState, url);
break;
}
case ClusterStatus.Connected: {
fetch(setState, url);
break;
}
}
}
}, [clusterStatus, url]); // eslint-disable-line react-hooks/exhaustive-deps
@ -48,17 +55,19 @@ async function fetch(dispatch: Dispatch, url: string) {
try {
const connection = new Connection(url, "max");
const supply = (await connection.getSupply()).value;
const accounts = (await connection.getLargestAccounts()).value;
const [total, circulating, nonCirculating] = (
await Promise.all([
connection.getLargestAccounts(),
connection.getLargestAccounts({ filter: "circulating" }),
connection.getLargestAccounts({ filter: "nonCirculating" })
])
).map(response => response.value);
// Update state if still connecting
dispatch(state => {
if (state !== Status.Connecting) return state;
return {
accounts,
totalSupply: supply.total,
circulatingSupply: supply.circulating
};
return { total, circulating, nonCirculating };
});
} catch (err) {
console.error("Failed to fetch", err);