Add navbar and remove tabbed page layout
This commit is contained in:
parent
eab48c99d7
commit
d4eb49d252
@ -1,59 +1,33 @@
|
||||
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 TransactionsCard from "./components/TransactionsCard";
|
||||
import TransactionDetails from "./components/TransactionDetails";
|
||||
import ClusterModal from "./components/ClusterModal";
|
||||
import Logo from "./img/logos-solana/light-explorer-logo.svg";
|
||||
import { TX_ALIASES } from "./providers/transactions";
|
||||
import { ACCOUNT_ALIASES, ACCOUNT_ALIASES_PLURAL } from "./providers/accounts";
|
||||
import TabbedPage from "components/TabbedPage";
|
||||
import TopAccountsCard from "components/TopAccountsCard";
|
||||
import SupplyCard from "components/SupplyCard";
|
||||
import StatsCard from "components/StatsCard";
|
||||
import { pickCluster } from "utils/url";
|
||||
import Banner from "components/Banner";
|
||||
import MessageBanner from "components/MessageBanner";
|
||||
import Navbar from "components/Navbar";
|
||||
import { ClusterStatusBanner } from "components/ClusterStatusButton";
|
||||
|
||||
function App() {
|
||||
return (
|
||||
<>
|
||||
<ClusterModal />
|
||||
<div className="main-content">
|
||||
<nav className="navbar navbar-expand-xl navbar-light">
|
||||
<div className="container">
|
||||
<div className="row align-items-end">
|
||||
<div className="col">
|
||||
<Link
|
||||
to={(location) => ({
|
||||
...pickCluster(location),
|
||||
pathname: "/",
|
||||
})}
|
||||
>
|
||||
<img src={Logo} width="250" alt="Solana Explorer" />
|
||||
</Link>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</nav>
|
||||
|
||||
<Banner />
|
||||
|
||||
<Navbar />
|
||||
<MessageBanner />
|
||||
<ClusterStatusBanner />
|
||||
<Switch>
|
||||
<Route exact path="/supply">
|
||||
<TabbedPage tab="Supply">
|
||||
<Route exact path={["/supply", "/accounts", "accounts/top"]}>
|
||||
<div className="container mt-4">
|
||||
<SupplyCard />
|
||||
<TopAccountsCard />
|
||||
</TabbedPage>
|
||||
</div>
|
||||
</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(
|
||||
@ -63,11 +37,6 @@ function App() {
|
||||
<TransactionDetails signature={match.params.signature} />
|
||||
)}
|
||||
/>
|
||||
<Route exact path={TX_ALIASES.map((tx) => `/${tx}s`)}>
|
||||
<TabbedPage tab="Transactions">
|
||||
<TransactionsCard />
|
||||
</TabbedPage>
|
||||
</Route>
|
||||
<Route
|
||||
exact
|
||||
path={ACCOUNT_ALIASES.concat(ACCOUNT_ALIASES_PLURAL).map(
|
||||
@ -77,19 +46,16 @@ function App() {
|
||||
<AccountDetails address={match.params.address} />
|
||||
)}
|
||||
/>
|
||||
<Route
|
||||
exact
|
||||
path={ACCOUNT_ALIASES_PLURAL.map((alias) => "/" + alias)}
|
||||
>
|
||||
<TabbedPage tab="Accounts">
|
||||
<AccountsCard />
|
||||
</TabbedPage>
|
||||
</Route>
|
||||
<Route>
|
||||
<TabbedPage tab="Stats">
|
||||
<Route exact path="/">
|
||||
<div className="container mt-4">
|
||||
<StatsCard />
|
||||
</TabbedPage>
|
||||
</div>
|
||||
</Route>
|
||||
<Route
|
||||
render={({ location }) => (
|
||||
<Redirect to={{ ...location, pathname: "/" }} />
|
||||
)}
|
||||
/>
|
||||
</Switch>
|
||||
</div>
|
||||
</>
|
||||
|
@ -1,8 +1,6 @@
|
||||
import React from "react";
|
||||
import { Link } from "react-router-dom";
|
||||
import { useClusterModal } from "providers/cluster";
|
||||
import { PublicKey, StakeProgram } from "@solana/web3.js";
|
||||
import ClusterStatusButton from "components/ClusterStatusButton";
|
||||
import { useHistory, useLocation } from "react-router-dom";
|
||||
import {
|
||||
FetchStatus,
|
||||
@ -23,7 +21,6 @@ import { useFetchAccountHistory } from "providers/accounts/history";
|
||||
type Props = { address: string };
|
||||
export default function AccountDetails({ address }: Props) {
|
||||
const fetchAccount = useFetchAccountInfo();
|
||||
const [, setShow] = useClusterModal();
|
||||
const [search, setSearch] = React.useState(address);
|
||||
const history = useHistory();
|
||||
const location = useLocation();
|
||||
@ -61,34 +58,10 @@ export default function AccountDetails({ address }: Props) {
|
||||
<div className="container">
|
||||
<div className="header">
|
||||
<div className="header-body">
|
||||
<div className="row align-items-center">
|
||||
<div className="col">
|
||||
<h6 className="header-pretitle">Details</h6>
|
||||
<h3 className="header-title">Account</h3>
|
||||
</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>
|
||||
<h6 className="header-pretitle">Address</h6>
|
||||
<h4 className="header-title text-monospace text-truncate font-weight-bold">
|
||||
{address}
|
||||
</h4>
|
||||
</div>
|
||||
</div>
|
||||
{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 { 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 (
|
||||
<div onClick={onClick}>
|
||||
<Button expand={expand} />
|
||||
<div className="container d-md-none my-4">
|
||||
<div onClick={() => setShow(true)}>
|
||||
<Button />
|
||||
</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 statusName = cluster !== Cluster.Custom ? `${name}` : `${customUrl}`;
|
||||
|
||||
const btnClasses = (variant: string) => {
|
||||
if (expand) {
|
||||
return `btn lift d-block btn-${variant}`;
|
||||
} else {
|
||||
return `btn b-white lift btn-outline-${variant}`;
|
||||
}
|
||||
return `btn d-block btn-${variant}`;
|
||||
};
|
||||
|
||||
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="row align-items-center">
|
||||
<div className="col">
|
||||
<h4 className="card-header-title">Live Cluster Info</h4>
|
||||
<h4 className="card-header-title">Live Cluster Status</h4>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
@ -32,9 +32,7 @@ export default function SupplyCard() {
|
||||
<TableCardBody>
|
||||
<tr>
|
||||
<td className="w-100">Total Supply (SOL)</td>
|
||||
<td className="text-right">
|
||||
{lamportsToSolString(supply.total, 0)}
|
||||
</td>
|
||||
<td className="text-right">{lamportsToSolString(supply.total, 0)}</td>
|
||||
</tr>
|
||||
|
||||
<tr>
|
||||
@ -60,7 +58,7 @@ const renderHeader = () => {
|
||||
<div className="card-header">
|
||||
<div className="row align-items-center">
|
||||
<div className="col">
|
||||
<h4 className="card-header-title">Overview</h4>
|
||||
<h4 className="card-header-title">Supply Overview</h4>
|
||||
</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>
|
||||
</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>
|
||||
<Link
|
||||
to={(location) => ({
|
||||
|
@ -6,18 +6,16 @@ import {
|
||||
FetchStatus,
|
||||
} from "../providers/transactions";
|
||||
import { useFetchTransactionDetails } from "providers/transactions/details";
|
||||
import { useCluster, useClusterModal } from "providers/cluster";
|
||||
import { useCluster } from "providers/cluster";
|
||||
import {
|
||||
TransactionSignature,
|
||||
SystemProgram,
|
||||
StakeProgram,
|
||||
SystemInstruction,
|
||||
} from "@solana/web3.js";
|
||||
import ClusterStatusButton from "components/ClusterStatusButton";
|
||||
import { lamportsToSolString } from "utils";
|
||||
import { displayAddress } from "utils/tx";
|
||||
import Copyable from "./Copyable";
|
||||
import { useHistory, useLocation } from "react-router-dom";
|
||||
import { UnknownDetailsCard } from "./instruction/UnknownDetailsCard";
|
||||
import { SystemDetailsCard } from "./instruction/system/SystemDetailsCard";
|
||||
import { StakeDetailsCard } from "./instruction/stake/StakeDetailsCard";
|
||||
@ -31,63 +29,20 @@ import { isCached } from "providers/transactions/cached";
|
||||
type Props = { signature: TransactionSignature };
|
||||
export default function TransactionDetails({ signature }: Props) {
|
||||
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
|
||||
React.useEffect(() => {
|
||||
fetchTransaction(signature);
|
||||
}, [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 (
|
||||
<div className="container">
|
||||
<div className="header">
|
||||
<div className="header-body">
|
||||
<div className="row align-items-center">
|
||||
<div className="col">
|
||||
<h6 className="header-pretitle">Details</h6>
|
||||
<h3 className="header-title">Transaction</h3>
|
||||
</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>
|
||||
<h6 className="header-pretitle">Transaction</h6>
|
||||
<h4 className="header-title text-monospace text-truncate font-weight-bold">
|
||||
{signature}
|
||||
</h4>
|
||||
</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-5: #1dd79b;
|
||||
|
||||
$primary: #65D39F;
|
||||
$success: #42ba96;
|
||||
$primary: $success;
|
||||
$primary-desat: #42ba96;
|
||||
$secondary: $gray-700;
|
||||
$success: #42ba96;
|
||||
$info: #b45be1;
|
||||
$info-muted: #9272a3;
|
||||
$warning: #d83aeb;
|
||||
|
Loading…
x
Reference in New Issue
Block a user