Add navbar and remove tabbed page layout
This commit is contained in:
committed by
Justin Starry
parent
eab48c99d7
commit
d4eb49d252
@ -1,59 +1,33 @@
|
|||||||
import React from "react";
|
import React from "react";
|
||||||
import { Link, Switch, Route, Redirect } from "react-router-dom";
|
import { Switch, Route, Redirect } from "react-router-dom";
|
||||||
|
|
||||||
import AccountsCard from "./components/AccountsCard";
|
|
||||||
import AccountDetails from "./components/AccountDetails";
|
import AccountDetails from "./components/AccountDetails";
|
||||||
import TransactionsCard from "./components/TransactionsCard";
|
|
||||||
import TransactionDetails from "./components/TransactionDetails";
|
import TransactionDetails from "./components/TransactionDetails";
|
||||||
import ClusterModal from "./components/ClusterModal";
|
import ClusterModal from "./components/ClusterModal";
|
||||||
import Logo from "./img/logos-solana/light-explorer-logo.svg";
|
|
||||||
import { TX_ALIASES } from "./providers/transactions";
|
import { TX_ALIASES } from "./providers/transactions";
|
||||||
import { ACCOUNT_ALIASES, ACCOUNT_ALIASES_PLURAL } from "./providers/accounts";
|
import { ACCOUNT_ALIASES, ACCOUNT_ALIASES_PLURAL } from "./providers/accounts";
|
||||||
import TabbedPage from "components/TabbedPage";
|
|
||||||
import TopAccountsCard from "components/TopAccountsCard";
|
import TopAccountsCard from "components/TopAccountsCard";
|
||||||
import SupplyCard from "components/SupplyCard";
|
import SupplyCard from "components/SupplyCard";
|
||||||
import StatsCard from "components/StatsCard";
|
import StatsCard from "components/StatsCard";
|
||||||
import { pickCluster } from "utils/url";
|
import MessageBanner from "components/MessageBanner";
|
||||||
import Banner from "components/Banner";
|
import Navbar from "components/Navbar";
|
||||||
|
import { ClusterStatusBanner } from "components/ClusterStatusButton";
|
||||||
|
|
||||||
function App() {
|
function App() {
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<ClusterModal />
|
<ClusterModal />
|
||||||
<div className="main-content">
|
<div className="main-content">
|
||||||
<nav className="navbar navbar-expand-xl navbar-light">
|
<Navbar />
|
||||||
<div className="container">
|
<MessageBanner />
|
||||||
<div className="row align-items-end">
|
<ClusterStatusBanner />
|
||||||
<div className="col">
|
|
||||||
<Link
|
|
||||||
to={(location) => ({
|
|
||||||
...pickCluster(location),
|
|
||||||
pathname: "/",
|
|
||||||
})}
|
|
||||||
>
|
|
||||||
<img src={Logo} width="250" alt="Solana Explorer" />
|
|
||||||
</Link>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</nav>
|
|
||||||
|
|
||||||
<Banner />
|
|
||||||
|
|
||||||
<Switch>
|
<Switch>
|
||||||
<Route exact path="/supply">
|
<Route exact path={["/supply", "/accounts", "accounts/top"]}>
|
||||||
<TabbedPage tab="Supply">
|
<div className="container mt-4">
|
||||||
<SupplyCard />
|
<SupplyCard />
|
||||||
<TopAccountsCard />
|
<TopAccountsCard />
|
||||||
</TabbedPage>
|
</div>
|
||||||
</Route>
|
</Route>
|
||||||
<Route
|
|
||||||
exact
|
|
||||||
path="/accounts/top"
|
|
||||||
render={({ location }) => (
|
|
||||||
<Redirect to={{ ...location, pathname: "/supply" }} />
|
|
||||||
)}
|
|
||||||
></Route>
|
|
||||||
<Route
|
<Route
|
||||||
exact
|
exact
|
||||||
path={TX_ALIASES.flatMap((tx) => [tx, tx + "s"]).map(
|
path={TX_ALIASES.flatMap((tx) => [tx, tx + "s"]).map(
|
||||||
@ -63,11 +37,6 @@ function App() {
|
|||||||
<TransactionDetails signature={match.params.signature} />
|
<TransactionDetails signature={match.params.signature} />
|
||||||
)}
|
)}
|
||||||
/>
|
/>
|
||||||
<Route exact path={TX_ALIASES.map((tx) => `/${tx}s`)}>
|
|
||||||
<TabbedPage tab="Transactions">
|
|
||||||
<TransactionsCard />
|
|
||||||
</TabbedPage>
|
|
||||||
</Route>
|
|
||||||
<Route
|
<Route
|
||||||
exact
|
exact
|
||||||
path={ACCOUNT_ALIASES.concat(ACCOUNT_ALIASES_PLURAL).map(
|
path={ACCOUNT_ALIASES.concat(ACCOUNT_ALIASES_PLURAL).map(
|
||||||
@ -77,19 +46,16 @@ function App() {
|
|||||||
<AccountDetails address={match.params.address} />
|
<AccountDetails address={match.params.address} />
|
||||||
)}
|
)}
|
||||||
/>
|
/>
|
||||||
<Route
|
<Route exact path="/">
|
||||||
exact
|
<div className="container mt-4">
|
||||||
path={ACCOUNT_ALIASES_PLURAL.map((alias) => "/" + alias)}
|
|
||||||
>
|
|
||||||
<TabbedPage tab="Accounts">
|
|
||||||
<AccountsCard />
|
|
||||||
</TabbedPage>
|
|
||||||
</Route>
|
|
||||||
<Route>
|
|
||||||
<TabbedPage tab="Stats">
|
|
||||||
<StatsCard />
|
<StatsCard />
|
||||||
</TabbedPage>
|
</div>
|
||||||
</Route>
|
</Route>
|
||||||
|
<Route
|
||||||
|
render={({ location }) => (
|
||||||
|
<Redirect to={{ ...location, pathname: "/" }} />
|
||||||
|
)}
|
||||||
|
/>
|
||||||
</Switch>
|
</Switch>
|
||||||
</div>
|
</div>
|
||||||
</>
|
</>
|
||||||
|
@ -1,8 +1,6 @@
|
|||||||
import React from "react";
|
import React from "react";
|
||||||
import { Link } from "react-router-dom";
|
import { Link } from "react-router-dom";
|
||||||
import { useClusterModal } from "providers/cluster";
|
|
||||||
import { PublicKey, StakeProgram } from "@solana/web3.js";
|
import { PublicKey, StakeProgram } from "@solana/web3.js";
|
||||||
import ClusterStatusButton from "components/ClusterStatusButton";
|
|
||||||
import { useHistory, useLocation } from "react-router-dom";
|
import { useHistory, useLocation } from "react-router-dom";
|
||||||
import {
|
import {
|
||||||
FetchStatus,
|
FetchStatus,
|
||||||
@ -23,7 +21,6 @@ import { useFetchAccountHistory } from "providers/accounts/history";
|
|||||||
type Props = { address: string };
|
type Props = { address: string };
|
||||||
export default function AccountDetails({ address }: Props) {
|
export default function AccountDetails({ address }: Props) {
|
||||||
const fetchAccount = useFetchAccountInfo();
|
const fetchAccount = useFetchAccountInfo();
|
||||||
const [, setShow] = useClusterModal();
|
|
||||||
const [search, setSearch] = React.useState(address);
|
const [search, setSearch] = React.useState(address);
|
||||||
const history = useHistory();
|
const history = useHistory();
|
||||||
const location = useLocation();
|
const location = useLocation();
|
||||||
@ -61,34 +58,10 @@ export default function AccountDetails({ address }: Props) {
|
|||||||
<div className="container">
|
<div className="container">
|
||||||
<div className="header">
|
<div className="header">
|
||||||
<div className="header-body">
|
<div className="header-body">
|
||||||
<div className="row align-items-center">
|
<h6 className="header-pretitle">Address</h6>
|
||||||
<div className="col">
|
<h4 className="header-title text-monospace text-truncate font-weight-bold">
|
||||||
<h6 className="header-pretitle">Details</h6>
|
{address}
|
||||||
<h3 className="header-title">Account</h3>
|
</h4>
|
||||||
</div>
|
|
||||||
<div className="col-auto">
|
|
||||||
<ClusterStatusButton onClick={() => setShow(true)} />
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="row mb-4 mt-n2 align-items-center">
|
|
||||||
<div className="col d-none d-md-block">
|
|
||||||
<div className="input-group input-group-merge">
|
|
||||||
{searchInput}
|
|
||||||
<div className="input-group-prepend">
|
|
||||||
<div className="input-group-text">
|
|
||||||
<span className="fe fe-search"></span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div className="col d-block d-md-none">{searchInput}</div>
|
|
||||||
<div className="col-auto ml-n3 d-block d-md-none">
|
|
||||||
<button className="btn btn-white" onClick={updateAddress}>
|
|
||||||
<span className="fe fe-search"></span>
|
|
||||||
</button>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{pubkey && <AccountCards pubkey={pubkey} />}
|
{pubkey && <AccountCards pubkey={pubkey} />}
|
||||||
|
@ -1,188 +0,0 @@
|
|||||||
import React, { ReactNode } from "react";
|
|
||||||
import { Link } from "react-router-dom";
|
|
||||||
import {
|
|
||||||
useAccounts,
|
|
||||||
Account,
|
|
||||||
FetchStatus,
|
|
||||||
useFetchAccountInfo,
|
|
||||||
} from "../providers/accounts";
|
|
||||||
import { assertUnreachable } from "../utils";
|
|
||||||
import { displayAddress } from "../utils/tx";
|
|
||||||
import { PublicKey } from "@solana/web3.js";
|
|
||||||
import Copyable from "./Copyable";
|
|
||||||
import { lamportsToSolString } from "utils";
|
|
||||||
|
|
||||||
function AccountsCard() {
|
|
||||||
const { accounts, idCounter } = useAccounts();
|
|
||||||
const fetchAccountInfo = useFetchAccountInfo();
|
|
||||||
const addressInput = React.useRef<HTMLInputElement>(null);
|
|
||||||
const [error, setError] = React.useState("");
|
|
||||||
|
|
||||||
const onNew = (address: string) => {
|
|
||||||
if (address.length === 0) return;
|
|
||||||
let pubkey;
|
|
||||||
try {
|
|
||||||
pubkey = new PublicKey(address);
|
|
||||||
} catch (err) {
|
|
||||||
setError(`${err}`);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
fetchAccountInfo(pubkey);
|
|
||||||
const inputEl = addressInput.current;
|
|
||||||
if (inputEl) {
|
|
||||||
inputEl.value = "";
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className="card">
|
|
||||||
{renderHeader()}
|
|
||||||
|
|
||||||
<div className="table-responsive mb-0">
|
|
||||||
<table className="table table-sm table-nowrap card-table">
|
|
||||||
<thead>
|
|
||||||
<tr>
|
|
||||||
<th className="text-muted">
|
|
||||||
<span className="fe fe-hash"></span>
|
|
||||||
</th>
|
|
||||||
<th className="text-muted">Status</th>
|
|
||||||
<th className="text-muted">Address</th>
|
|
||||||
<th className="text-muted">Balance (SOL)</th>
|
|
||||||
<th className="text-muted">Data (bytes)</th>
|
|
||||||
<th className="text-muted">Owner</th>
|
|
||||||
<th className="text-muted">Details</th>
|
|
||||||
</tr>
|
|
||||||
</thead>
|
|
||||||
<tbody className="list">
|
|
||||||
<tr>
|
|
||||||
<td>
|
|
||||||
<span className="badge badge-soft-dark badge-pill">
|
|
||||||
{idCounter + 1}
|
|
||||||
</span>
|
|
||||||
</td>
|
|
||||||
<td>
|
|
||||||
<span className={`badge badge-soft-dark`}>New</span>
|
|
||||||
</td>
|
|
||||||
<td>
|
|
||||||
<input
|
|
||||||
type="text"
|
|
||||||
onInput={() => setError("")}
|
|
||||||
onKeyDown={(e) =>
|
|
||||||
e.keyCode === 13 && onNew(e.currentTarget.value)
|
|
||||||
}
|
|
||||||
onSubmit={(e) => onNew(e.currentTarget.value)}
|
|
||||||
ref={addressInput}
|
|
||||||
className={`form-control text-address text-monospace ${
|
|
||||||
error ? "is-invalid" : ""
|
|
||||||
}`}
|
|
||||||
placeholder="input account address"
|
|
||||||
/>
|
|
||||||
{error ? <div className="invalid-feedback">{error}</div> : null}
|
|
||||||
</td>
|
|
||||||
<td>-</td>
|
|
||||||
<td>-</td>
|
|
||||||
<td>-</td>
|
|
||||||
<td></td>
|
|
||||||
</tr>
|
|
||||||
{accounts.map((account) => renderAccountRow(account))}
|
|
||||||
</tbody>
|
|
||||||
</table>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
const renderHeader = () => {
|
|
||||||
return (
|
|
||||||
<div className="card-header">
|
|
||||||
<div className="row align-items-center">
|
|
||||||
<div className="col">
|
|
||||||
<h4 className="card-header-title">Look Up Account(s)</h4>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
const renderAccountRow = (account: Account) => {
|
|
||||||
let statusText;
|
|
||||||
let statusClass;
|
|
||||||
switch (account.status) {
|
|
||||||
case FetchStatus.FetchFailed:
|
|
||||||
statusClass = "dark";
|
|
||||||
statusText = "Cluster Error";
|
|
||||||
break;
|
|
||||||
case FetchStatus.Fetching:
|
|
||||||
statusClass = "info";
|
|
||||||
statusText = "Fetching";
|
|
||||||
break;
|
|
||||||
case FetchStatus.Fetched:
|
|
||||||
if (account.details?.executable) {
|
|
||||||
statusClass = "dark";
|
|
||||||
statusText = "Executable";
|
|
||||||
} else if (account.lamports) {
|
|
||||||
statusClass = "success";
|
|
||||||
statusText = "Found";
|
|
||||||
} else {
|
|
||||||
statusClass = "danger";
|
|
||||||
statusText = "Not Found";
|
|
||||||
}
|
|
||||||
break;
|
|
||||||
default:
|
|
||||||
return assertUnreachable(account.status);
|
|
||||||
}
|
|
||||||
|
|
||||||
let data = "-";
|
|
||||||
let owner = "-";
|
|
||||||
if (account.details) {
|
|
||||||
data = `${account.details.space}`;
|
|
||||||
owner = displayAddress(account.details.owner.toBase58());
|
|
||||||
}
|
|
||||||
|
|
||||||
let balance: ReactNode = "-";
|
|
||||||
if (account.lamports !== undefined) {
|
|
||||||
balance = lamportsToSolString(account.lamports);
|
|
||||||
}
|
|
||||||
|
|
||||||
const base58AccountPubkey = account.pubkey.toBase58();
|
|
||||||
return (
|
|
||||||
<tr key={account.id}>
|
|
||||||
<td>
|
|
||||||
<span className="badge badge-soft-dark badge-pill">{account.id}</span>
|
|
||||||
</td>
|
|
||||||
<td>
|
|
||||||
<span className={`badge badge-soft-${statusClass}`}>{statusText}</span>
|
|
||||||
</td>
|
|
||||||
<td>
|
|
||||||
<Copyable text={base58AccountPubkey}>
|
|
||||||
<code>{base58AccountPubkey}</code>
|
|
||||||
</Copyable>
|
|
||||||
</td>
|
|
||||||
<td>{balance}</td>
|
|
||||||
<td>{data}</td>
|
|
||||||
<td>
|
|
||||||
{owner === "-" ? (
|
|
||||||
owner
|
|
||||||
) : (
|
|
||||||
<Copyable text={owner}>
|
|
||||||
<code>{owner}</code>
|
|
||||||
</Copyable>
|
|
||||||
)}
|
|
||||||
</td>
|
|
||||||
<td>
|
|
||||||
<Link
|
|
||||||
to={(location) => ({
|
|
||||||
...location,
|
|
||||||
pathname: "/account/" + base58AccountPubkey,
|
|
||||||
})}
|
|
||||||
className="btn btn-rounded-circle btn-white btn-sm"
|
|
||||||
>
|
|
||||||
<span className="fe fe-arrow-right"></span>
|
|
||||||
</Link>
|
|
||||||
</td>
|
|
||||||
</tr>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export default AccountsCard;
|
|
@ -1,30 +1,39 @@
|
|||||||
import React from "react";
|
import React from "react";
|
||||||
import { useCluster, ClusterStatus, Cluster } from "../providers/cluster";
|
import {
|
||||||
|
useCluster,
|
||||||
|
ClusterStatus,
|
||||||
|
Cluster,
|
||||||
|
useClusterModal,
|
||||||
|
} from "../providers/cluster";
|
||||||
|
|
||||||
|
export function ClusterStatusBanner() {
|
||||||
|
const [, setShow] = useClusterModal();
|
||||||
|
|
||||||
function ClusterStatusButton({
|
|
||||||
onClick,
|
|
||||||
expand,
|
|
||||||
}: {
|
|
||||||
onClick: () => void;
|
|
||||||
expand?: boolean;
|
|
||||||
}) {
|
|
||||||
return (
|
return (
|
||||||
<div onClick={onClick}>
|
<div className="container d-md-none my-4">
|
||||||
<Button expand={expand} />
|
<div onClick={() => setShow(true)}>
|
||||||
|
<Button />
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
function Button({ expand }: { expand?: boolean }) {
|
export function ClusterStatusButton() {
|
||||||
|
const [, setShow] = useClusterModal();
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div onClick={() => setShow(true)}>
|
||||||
|
<Button />
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function Button() {
|
||||||
const { status, cluster, name, customUrl } = useCluster();
|
const { status, cluster, name, customUrl } = useCluster();
|
||||||
const statusName = cluster !== Cluster.Custom ? `${name}` : `${customUrl}`;
|
const statusName = cluster !== Cluster.Custom ? `${name}` : `${customUrl}`;
|
||||||
|
|
||||||
const btnClasses = (variant: string) => {
|
const btnClasses = (variant: string) => {
|
||||||
if (expand) {
|
return `btn d-block btn-${variant}`;
|
||||||
return `btn lift d-block btn-${variant}`;
|
|
||||||
} else {
|
|
||||||
return `btn b-white lift btn-outline-${variant}`;
|
|
||||||
}
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const spinnerClasses = "spinner-grow spinner-grow-sm mr-2";
|
const spinnerClasses = "spinner-grow spinner-grow-sm mr-2";
|
||||||
@ -59,5 +68,3 @@ function Button({ expand }: { expand?: boolean }) {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export default ClusterStatusButton;
|
|
||||||
|
55
explorer/src/components/Navbar.tsx
Normal file
55
explorer/src/components/Navbar.tsx
Normal file
@ -0,0 +1,55 @@
|
|||||||
|
import React from "react";
|
||||||
|
import Logo from "img/logos-solana/light-explorer-logo.svg";
|
||||||
|
import { Location } from "history";
|
||||||
|
import { pickCluster } from "utils/url";
|
||||||
|
import { Link, NavLink } from "react-router-dom";
|
||||||
|
import { ClusterStatusButton } from "components/ClusterStatusButton";
|
||||||
|
|
||||||
|
const clusterPath = (pathname: string) => {
|
||||||
|
return (location: Location) => ({
|
||||||
|
...pickCluster(location),
|
||||||
|
pathname,
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
export default function Navbar() {
|
||||||
|
// TODO: use `collapsing` to animate collapsible navbar
|
||||||
|
const [collapse, setCollapse] = React.useState(false);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<nav className="navbar navbar-expand-md navbar-light">
|
||||||
|
<div className="container">
|
||||||
|
<Link to={clusterPath("/")}>
|
||||||
|
<img src={Logo} width="250" alt="Solana Explorer" />
|
||||||
|
</Link>
|
||||||
|
|
||||||
|
<button
|
||||||
|
className="navbar-toggler"
|
||||||
|
type="button"
|
||||||
|
onClick={() => setCollapse((value) => !value)}
|
||||||
|
>
|
||||||
|
<span className="navbar-toggler-icon"></span>
|
||||||
|
</button>
|
||||||
|
|
||||||
|
<div className={`collapse navbar-collapse ml-auto mr-4 ${collapse ? "show" : ""}`}>
|
||||||
|
<ul className="navbar-nav mr-auto">
|
||||||
|
<li className="nav-item">
|
||||||
|
<NavLink className="nav-link" to={clusterPath("/")} exact>
|
||||||
|
Cluster Status
|
||||||
|
</NavLink>
|
||||||
|
</li>
|
||||||
|
<li className="nav-item">
|
||||||
|
<NavLink className="nav-link" to={clusterPath("/supply")}>
|
||||||
|
Supply
|
||||||
|
</NavLink>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="d-none d-md-block">
|
||||||
|
<ClusterStatusButton />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</nav>
|
||||||
|
);
|
||||||
|
}
|
@ -19,7 +19,7 @@ export default function StatsCard() {
|
|||||||
<div className="card-header">
|
<div className="card-header">
|
||||||
<div className="row align-items-center">
|
<div className="row align-items-center">
|
||||||
<div className="col">
|
<div className="col">
|
||||||
<h4 className="card-header-title">Live Cluster Info</h4>
|
<h4 className="card-header-title">Live Cluster Status</h4>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
@ -32,9 +32,7 @@ export default function SupplyCard() {
|
|||||||
<TableCardBody>
|
<TableCardBody>
|
||||||
<tr>
|
<tr>
|
||||||
<td className="w-100">Total Supply (SOL)</td>
|
<td className="w-100">Total Supply (SOL)</td>
|
||||||
<td className="text-right">
|
<td className="text-right">{lamportsToSolString(supply.total, 0)}</td>
|
||||||
{lamportsToSolString(supply.total, 0)}
|
|
||||||
</td>
|
|
||||||
</tr>
|
</tr>
|
||||||
|
|
||||||
<tr>
|
<tr>
|
||||||
@ -60,7 +58,7 @@ const renderHeader = () => {
|
|||||||
<div className="card-header">
|
<div className="card-header">
|
||||||
<div className="row align-items-center">
|
<div className="row align-items-center">
|
||||||
<div className="col">
|
<div className="col">
|
||||||
<h4 className="card-header-title">Overview</h4>
|
<h4 className="card-header-title">Supply Overview</h4>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
@ -1,77 +0,0 @@
|
|||||||
import React from "react";
|
|
||||||
import { Link } from "react-router-dom";
|
|
||||||
import { useClusterModal } from "providers/cluster";
|
|
||||||
import ClusterStatusButton from "components/ClusterStatusButton";
|
|
||||||
import { pickCluster } from "utils/url";
|
|
||||||
|
|
||||||
export type Tab = "Transactions" | "Accounts" | "Supply" | "Stats";
|
|
||||||
|
|
||||||
type Props = { children: React.ReactNode; tab: Tab };
|
|
||||||
export default function TabbedPage({ children, tab }: Props) {
|
|
||||||
const [, setShow] = useClusterModal();
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className="container">
|
|
||||||
<div className="header">
|
|
||||||
<div className="header-body">
|
|
||||||
<div className="row align-items-center d-md-none">
|
|
||||||
<div className="col-12">
|
|
||||||
<ClusterStatusButton expand onClick={() => setShow(true)} />
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div className="row align-items-center">
|
|
||||||
<div className="col">
|
|
||||||
<ul className="nav nav-tabs nav-overflow header-tabs">
|
|
||||||
<li className="nav-item">
|
|
||||||
<NavLink href="/" tab="Stats" current={tab} />
|
|
||||||
</li>
|
|
||||||
<li className="nav-item">
|
|
||||||
<NavLink
|
|
||||||
href="/transactions"
|
|
||||||
tab="Transactions"
|
|
||||||
current={tab}
|
|
||||||
/>
|
|
||||||
</li>
|
|
||||||
<li className="nav-item">
|
|
||||||
<NavLink href="/accounts" tab="Accounts" current={tab} />
|
|
||||||
</li>
|
|
||||||
<li className="nav-item">
|
|
||||||
<NavLink href="/supply" tab="Supply" current={tab} />
|
|
||||||
</li>
|
|
||||||
</ul>
|
|
||||||
</div>
|
|
||||||
<div className="col-auto d-none d-md-block">
|
|
||||||
<ClusterStatusButton onClick={() => setShow(true)} />
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{children}
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
function NavLink({
|
|
||||||
href,
|
|
||||||
tab,
|
|
||||||
current,
|
|
||||||
}: {
|
|
||||||
href: string;
|
|
||||||
tab: Tab;
|
|
||||||
current: Tab;
|
|
||||||
}) {
|
|
||||||
let classes = "nav-link";
|
|
||||||
if (tab === current) {
|
|
||||||
classes += " active";
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Link
|
|
||||||
to={(location) => ({ ...pickCluster(location), pathname: href })}
|
|
||||||
className={classes}
|
|
||||||
>
|
|
||||||
{tab}
|
|
||||||
</Link>
|
|
||||||
);
|
|
||||||
}
|
|
@ -124,7 +124,10 @@ const renderAccountRow = (
|
|||||||
</Copyable>
|
</Copyable>
|
||||||
</td>
|
</td>
|
||||||
<td className="text-right">{lamportsToSolString(account.lamports, 0)}</td>
|
<td className="text-right">{lamportsToSolString(account.lamports, 0)}</td>
|
||||||
<td className="text-center">{`${((100 * account.lamports) / supply).toFixed(3)}%`}</td>
|
<td className="text-center">{`${(
|
||||||
|
(100 * account.lamports) /
|
||||||
|
supply
|
||||||
|
).toFixed(3)}%`}</td>
|
||||||
<td>
|
<td>
|
||||||
<Link
|
<Link
|
||||||
to={(location) => ({
|
to={(location) => ({
|
||||||
|
@ -6,18 +6,16 @@ import {
|
|||||||
FetchStatus,
|
FetchStatus,
|
||||||
} from "../providers/transactions";
|
} from "../providers/transactions";
|
||||||
import { useFetchTransactionDetails } from "providers/transactions/details";
|
import { useFetchTransactionDetails } from "providers/transactions/details";
|
||||||
import { useCluster, useClusterModal } from "providers/cluster";
|
import { useCluster } from "providers/cluster";
|
||||||
import {
|
import {
|
||||||
TransactionSignature,
|
TransactionSignature,
|
||||||
SystemProgram,
|
SystemProgram,
|
||||||
StakeProgram,
|
StakeProgram,
|
||||||
SystemInstruction,
|
SystemInstruction,
|
||||||
} from "@solana/web3.js";
|
} from "@solana/web3.js";
|
||||||
import ClusterStatusButton from "components/ClusterStatusButton";
|
|
||||||
import { lamportsToSolString } from "utils";
|
import { lamportsToSolString } from "utils";
|
||||||
import { displayAddress } from "utils/tx";
|
import { displayAddress } from "utils/tx";
|
||||||
import Copyable from "./Copyable";
|
import Copyable from "./Copyable";
|
||||||
import { useHistory, useLocation } from "react-router-dom";
|
|
||||||
import { UnknownDetailsCard } from "./instruction/UnknownDetailsCard";
|
import { UnknownDetailsCard } from "./instruction/UnknownDetailsCard";
|
||||||
import { SystemDetailsCard } from "./instruction/system/SystemDetailsCard";
|
import { SystemDetailsCard } from "./instruction/system/SystemDetailsCard";
|
||||||
import { StakeDetailsCard } from "./instruction/stake/StakeDetailsCard";
|
import { StakeDetailsCard } from "./instruction/stake/StakeDetailsCard";
|
||||||
@ -31,63 +29,20 @@ import { isCached } from "providers/transactions/cached";
|
|||||||
type Props = { signature: TransactionSignature };
|
type Props = { signature: TransactionSignature };
|
||||||
export default function TransactionDetails({ signature }: Props) {
|
export default function TransactionDetails({ signature }: Props) {
|
||||||
const fetchTransaction = useFetchTransactionStatus();
|
const fetchTransaction = useFetchTransactionStatus();
|
||||||
const [, setShow] = useClusterModal();
|
|
||||||
const [search, setSearch] = React.useState(signature);
|
|
||||||
const history = useHistory();
|
|
||||||
const location = useLocation();
|
|
||||||
|
|
||||||
const updateSignature = () => {
|
|
||||||
history.push({ ...location, pathname: "/tx/" + search });
|
|
||||||
};
|
|
||||||
|
|
||||||
// Fetch transaction on load
|
// Fetch transaction on load
|
||||||
React.useEffect(() => {
|
React.useEffect(() => {
|
||||||
fetchTransaction(signature);
|
fetchTransaction(signature);
|
||||||
}, [signature]); // eslint-disable-line react-hooks/exhaustive-deps
|
}, [signature]); // eslint-disable-line react-hooks/exhaustive-deps
|
||||||
|
|
||||||
const searchInput = (
|
|
||||||
<input
|
|
||||||
type="text"
|
|
||||||
value={search}
|
|
||||||
onChange={(e) => setSearch(e.target.value)}
|
|
||||||
onKeyUp={(e) => e.key === "Enter" && updateSignature()}
|
|
||||||
className="form-control form-control-prepended search text-monospace"
|
|
||||||
placeholder="Search for signature"
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="container">
|
<div className="container">
|
||||||
<div className="header">
|
<div className="header">
|
||||||
<div className="header-body">
|
<div className="header-body">
|
||||||
<div className="row align-items-center">
|
<h6 className="header-pretitle">Transaction</h6>
|
||||||
<div className="col">
|
<h4 className="header-title text-monospace text-truncate font-weight-bold">
|
||||||
<h6 className="header-pretitle">Details</h6>
|
{signature}
|
||||||
<h3 className="header-title">Transaction</h3>
|
</h4>
|
||||||
</div>
|
|
||||||
<div className="col-auto">
|
|
||||||
<ClusterStatusButton onClick={() => setShow(true)} />
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="row mb-4 mt-n2 align-items-center">
|
|
||||||
<div className="col d-none d-md-block">
|
|
||||||
<div className="input-group input-group-merge">
|
|
||||||
{searchInput}
|
|
||||||
<div className="input-group-prepend">
|
|
||||||
<div className="input-group-text">
|
|
||||||
<span className="fe fe-search"></span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div className="col d-block d-md-none">{searchInput}</div>
|
|
||||||
<div className="col-auto ml-n3 d-block d-md-none">
|
|
||||||
<button className="btn btn-white" onClick={updateSignature}>
|
|
||||||
<span className="fe fe-search"></span>
|
|
||||||
</button>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
@ -1,177 +0,0 @@
|
|||||||
import React from "react";
|
|
||||||
import { Link } from "react-router-dom";
|
|
||||||
import {
|
|
||||||
useTransactions,
|
|
||||||
TransactionStatus,
|
|
||||||
FetchStatus,
|
|
||||||
useFetchTransactionStatus,
|
|
||||||
} from "../providers/transactions";
|
|
||||||
import bs58 from "bs58";
|
|
||||||
import { assertUnreachable } from "../utils";
|
|
||||||
import Copyable from "./Copyable";
|
|
||||||
|
|
||||||
function TransactionsCard() {
|
|
||||||
const { transactions, idCounter } = useTransactions();
|
|
||||||
const fetchTransaction = useFetchTransactionStatus();
|
|
||||||
const signatureInput = React.useRef<HTMLInputElement>(null);
|
|
||||||
const [error, setError] = React.useState("");
|
|
||||||
|
|
||||||
const onNew = (signature: string) => {
|
|
||||||
if (signature.length === 0) return;
|
|
||||||
try {
|
|
||||||
const length = bs58.decode(signature).length;
|
|
||||||
if (length > 64) {
|
|
||||||
setError("Signature is too long");
|
|
||||||
return;
|
|
||||||
} else if (length < 64) {
|
|
||||||
setError("Signature is too short");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
} catch (err) {
|
|
||||||
setError(`${err}`);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
fetchTransaction(signature);
|
|
||||||
const inputEl = signatureInput.current;
|
|
||||||
if (inputEl) {
|
|
||||||
inputEl.value = "";
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className="card">
|
|
||||||
{renderHeader()}
|
|
||||||
|
|
||||||
<div className="table-responsive mb-0">
|
|
||||||
<table className="table table-sm table-nowrap card-table">
|
|
||||||
<thead>
|
|
||||||
<tr>
|
|
||||||
<th className="text-muted">
|
|
||||||
<span className="fe fe-hash"></span>
|
|
||||||
</th>
|
|
||||||
<th className="text-muted">Status</th>
|
|
||||||
<th className="text-muted">Signature</th>
|
|
||||||
<th className="text-muted">Confirmations</th>
|
|
||||||
<th className="text-muted">Slot Number</th>
|
|
||||||
<th className="text-muted">Details</th>
|
|
||||||
</tr>
|
|
||||||
</thead>
|
|
||||||
<tbody className="list">
|
|
||||||
<tr>
|
|
||||||
<td>
|
|
||||||
<span className="badge badge-soft-dark badge-pill">
|
|
||||||
{idCounter + 1}
|
|
||||||
</span>
|
|
||||||
</td>
|
|
||||||
<td>
|
|
||||||
<span className={`badge badge-soft-dark`}>New</span>
|
|
||||||
</td>
|
|
||||||
<td>
|
|
||||||
<input
|
|
||||||
type="text"
|
|
||||||
onInput={() => setError("")}
|
|
||||||
onKeyDown={(e) =>
|
|
||||||
e.keyCode === 13 && onNew(e.currentTarget.value)
|
|
||||||
}
|
|
||||||
onSubmit={(e) => onNew(e.currentTarget.value)}
|
|
||||||
ref={signatureInput}
|
|
||||||
className={`form-control text-signature text-monospace ${
|
|
||||||
error ? "is-invalid" : ""
|
|
||||||
}`}
|
|
||||||
placeholder="input transaction signature"
|
|
||||||
/>
|
|
||||||
{error ? <div className="invalid-feedback">{error}</div> : null}
|
|
||||||
</td>
|
|
||||||
<td>-</td>
|
|
||||||
<td>-</td>
|
|
||||||
<td></td>
|
|
||||||
</tr>
|
|
||||||
{transactions.map((transaction) =>
|
|
||||||
renderTransactionRow(transaction)
|
|
||||||
)}
|
|
||||||
</tbody>
|
|
||||||
</table>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
const renderHeader = () => {
|
|
||||||
return (
|
|
||||||
<div className="card-header">
|
|
||||||
<div className="row align-items-center">
|
|
||||||
<div className="col">
|
|
||||||
<h4 className="card-header-title">Look Up Transaction(s)</h4>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
const renderTransactionRow = (transactionStatus: TransactionStatus) => {
|
|
||||||
const { fetchStatus, info, signature, id } = transactionStatus;
|
|
||||||
|
|
||||||
let statusText;
|
|
||||||
let statusClass;
|
|
||||||
switch (fetchStatus) {
|
|
||||||
case FetchStatus.FetchFailed:
|
|
||||||
statusClass = "dark";
|
|
||||||
statusText = "Cluster Error";
|
|
||||||
break;
|
|
||||||
case FetchStatus.Fetching:
|
|
||||||
statusClass = "info";
|
|
||||||
statusText = "Fetching";
|
|
||||||
break;
|
|
||||||
case FetchStatus.Fetched: {
|
|
||||||
if (!info) {
|
|
||||||
statusClass = "warning";
|
|
||||||
statusText = "Not Found";
|
|
||||||
} else if (info.result.err) {
|
|
||||||
statusClass = "danger";
|
|
||||||
statusText = "Failed";
|
|
||||||
} else {
|
|
||||||
statusClass = "success";
|
|
||||||
statusText = "Success";
|
|
||||||
}
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
default:
|
|
||||||
return assertUnreachable(fetchStatus);
|
|
||||||
}
|
|
||||||
|
|
||||||
let slotText = "-";
|
|
||||||
let confirmationsText = "-";
|
|
||||||
if (info) {
|
|
||||||
slotText = `${info.slot}`;
|
|
||||||
confirmationsText = `${info.confirmations}`;
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<tr key={signature}>
|
|
||||||
<td>
|
|
||||||
<span className="badge badge-soft-dark badge-pill">{id}</span>
|
|
||||||
</td>
|
|
||||||
<td>
|
|
||||||
<span className={`badge badge-soft-${statusClass}`}>{statusText}</span>
|
|
||||||
</td>
|
|
||||||
<td>
|
|
||||||
<Copyable text={signature}>
|
|
||||||
<code>{signature}</code>
|
|
||||||
</Copyable>
|
|
||||||
</td>
|
|
||||||
<td className="text-uppercase">{confirmationsText}</td>
|
|
||||||
<td>{slotText}</td>
|
|
||||||
<td>
|
|
||||||
<Link
|
|
||||||
to={(location) => ({ ...location, pathname: "/tx/" + signature })}
|
|
||||||
className="btn btn-rounded-circle btn-white btn-sm"
|
|
||||||
>
|
|
||||||
<span className="fe fe-arrow-right"></span>
|
|
||||||
</Link>
|
|
||||||
</td>
|
|
||||||
</tr>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export default TransactionsCard;
|
|
@ -31,10 +31,10 @@ $rainbow-3: #79abd2;
|
|||||||
$rainbow-4: #38d0bd;
|
$rainbow-4: #38d0bd;
|
||||||
$rainbow-5: #1dd79b;
|
$rainbow-5: #1dd79b;
|
||||||
|
|
||||||
$primary: #65D39F;
|
$success: #42ba96;
|
||||||
|
$primary: $success;
|
||||||
$primary-desat: #42ba96;
|
$primary-desat: #42ba96;
|
||||||
$secondary: $gray-700;
|
$secondary: $gray-700;
|
||||||
$success: #42ba96;
|
|
||||||
$info: #b45be1;
|
$info: #b45be1;
|
||||||
$info-muted: #9272a3;
|
$info-muted: #9272a3;
|
||||||
$warning: #d83aeb;
|
$warning: #d83aeb;
|
||||||
|
Reference in New Issue
Block a user