diff --git a/explorer/src/App.tsx b/explorer/src/App.tsx index c200895521..5612c91420 100644 --- a/explorer/src/App.tsx +++ b/explorer/src/App.tsx @@ -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 ( <>
- - - - + + + - - + +
- +
- ( - - )} - > [tx, tx + "s"]).map( @@ -63,11 +37,6 @@ function App() { )} /> - `/${tx}s`)}> - - - - )} /> - "/" + alias)} - > - - - - - - + +
- +
+ ( + + )} + />
diff --git a/explorer/src/components/AccountDetails.tsx b/explorer/src/components/AccountDetails.tsx index d6ec9bcc99..ee8b56194c 100644 --- a/explorer/src/components/AccountDetails.tsx +++ b/explorer/src/components/AccountDetails.tsx @@ -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) {
-
-
-
Details
-

Account

-
-
- setShow(true)} /> -
-
-
-
- -
-
-
- {searchInput} -
-
- -
-
-
-
-
{searchInput}
-
- +
Address
+

+ {address} +

{pubkey && } diff --git a/explorer/src/components/AccountsCard.tsx b/explorer/src/components/AccountsCard.tsx deleted file mode 100644 index e341cf21a0..0000000000 --- a/explorer/src/components/AccountsCard.tsx +++ /dev/null @@ -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(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 ( -
- {renderHeader()} - -
- - - - - - - - - - - - - - - - - - - - - - - {accounts.map((account) => renderAccountRow(account))} - -
- - StatusAddressBalance (SOL)Data (bytes)OwnerDetails
- - {idCounter + 1} - - - New - - 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 ?
{error}
: null} -
---
-
-
- ); -} - -const renderHeader = () => { - return ( -
-
-
-

Look Up Account(s)

-
-
-
- ); -}; - -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 ( - - - {account.id} - - - {statusText} - - - - {base58AccountPubkey} - - - {balance} - {data} - - {owner === "-" ? ( - owner - ) : ( - - {owner} - - )} - - - ({ - ...location, - pathname: "/account/" + base58AccountPubkey, - })} - className="btn btn-rounded-circle btn-white btn-sm" - > - - - - - ); -}; - -export default AccountsCard; diff --git a/explorer/src/components/ClusterStatusButton.tsx b/explorer/src/components/ClusterStatusButton.tsx index 80e4a88bd6..9d460072ce 100644 --- a/explorer/src/components/ClusterStatusButton.tsx +++ b/explorer/src/components/ClusterStatusButton.tsx @@ -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 ( -
-
); } -function Button({ expand }: { expand?: boolean }) { +export function ClusterStatusButton() { + const [, setShow] = useClusterModal(); + + return ( +
setShow(true)}> +
+ ); +} + +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; diff --git a/explorer/src/components/Banner.tsx b/explorer/src/components/MessageBanner.tsx similarity index 100% rename from explorer/src/components/Banner.tsx rename to explorer/src/components/MessageBanner.tsx diff --git a/explorer/src/components/Navbar.tsx b/explorer/src/components/Navbar.tsx new file mode 100644 index 0000000000..92c22feb03 --- /dev/null +++ b/explorer/src/components/Navbar.tsx @@ -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 ( + + ); +} diff --git a/explorer/src/components/StatsCard.tsx b/explorer/src/components/StatsCard.tsx index aa9ae74882..fd1a4a62f6 100644 --- a/explorer/src/components/StatsCard.tsx +++ b/explorer/src/components/StatsCard.tsx @@ -19,7 +19,7 @@ export default function StatsCard() {
-

Live Cluster Info

+

Live Cluster Status

diff --git a/explorer/src/components/SupplyCard.tsx b/explorer/src/components/SupplyCard.tsx index 3671867a2b..773714e525 100644 --- a/explorer/src/components/SupplyCard.tsx +++ b/explorer/src/components/SupplyCard.tsx @@ -32,9 +32,7 @@ export default function SupplyCard() { Total Supply (SOL) - - {lamportsToSolString(supply.total, 0)} - + {lamportsToSolString(supply.total, 0)} @@ -60,7 +58,7 @@ const renderHeader = () => {
-

Overview

+

Supply Overview

diff --git a/explorer/src/components/TabbedPage.tsx b/explorer/src/components/TabbedPage.tsx deleted file mode 100644 index 87fbfc0827..0000000000 --- a/explorer/src/components/TabbedPage.tsx +++ /dev/null @@ -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 ( -
-
-
-
-
- setShow(true)} /> -
-
-
-
-
    -
  • - -
  • -
  • - -
  • -
  • - -
  • -
  • - -
  • -
-
-
- setShow(true)} /> -
-
-
-
- - {children} -
- ); -} - -function NavLink({ - href, - tab, - current, -}: { - href: string; - tab: Tab; - current: Tab; -}) { - let classes = "nav-link"; - if (tab === current) { - classes += " active"; - } - - return ( - ({ ...pickCluster(location), pathname: href })} - className={classes} - > - {tab} - - ); -} diff --git a/explorer/src/components/TopAccountsCard.tsx b/explorer/src/components/TopAccountsCard.tsx index 311f8dbaf7..3cbb293be9 100644 --- a/explorer/src/components/TopAccountsCard.tsx +++ b/explorer/src/components/TopAccountsCard.tsx @@ -124,7 +124,10 @@ const renderAccountRow = ( {lamportsToSolString(account.lamports, 0)} - {`${((100 * account.lamports) / supply).toFixed(3)}%`} + {`${( + (100 * account.lamports) / + supply + ).toFixed(3)}%`} ({ diff --git a/explorer/src/components/TransactionDetails.tsx b/explorer/src/components/TransactionDetails.tsx index d74e033e74..1d1ddda495 100644 --- a/explorer/src/components/TransactionDetails.tsx +++ b/explorer/src/components/TransactionDetails.tsx @@ -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 = ( - setSearch(e.target.value)} - onKeyUp={(e) => e.key === "Enter" && updateSignature()} - className="form-control form-control-prepended search text-monospace" - placeholder="Search for signature" - /> - ); - return (
-
-
-
Details
-

Transaction

-
-
- setShow(true)} /> -
-
-
-
- -
-
-
- {searchInput} -
-
- -
-
-
-
-
{searchInput}
-
- +
Transaction
+

+ {signature} +

diff --git a/explorer/src/components/TransactionsCard.tsx b/explorer/src/components/TransactionsCard.tsx deleted file mode 100644 index 22b81cbdda..0000000000 --- a/explorer/src/components/TransactionsCard.tsx +++ /dev/null @@ -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(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 ( -
- {renderHeader()} - -
- - - - - - - - - - - - - - - - - - - - - {transactions.map((transaction) => - renderTransactionRow(transaction) - )} - -
- - StatusSignatureConfirmationsSlot NumberDetails
- - {idCounter + 1} - - - New - - 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 ?
{error}
: null} -
--
-
-
- ); -} - -const renderHeader = () => { - return ( -
-
-
-

Look Up Transaction(s)

-
-
-
- ); -}; - -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 ( - - - {id} - - - {statusText} - - - - {signature} - - - {confirmationsText} - {slotText} - - ({ ...location, pathname: "/tx/" + signature })} - className="btn btn-rounded-circle btn-white btn-sm" - > - - - - - ); -}; - -export default TransactionsCard; diff --git a/explorer/src/scss/_solana-variables.scss b/explorer/src/scss/_solana-variables.scss index 7d409738b0..154f8e1612 100644 --- a/explorer/src/scss/_solana-variables.scss +++ b/explorer/src/scss/_solana-variables.scss @@ -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;