Add unified search bar to the explorer

This commit is contained in:
Justin Starry
2020-08-02 01:46:22 +08:00
committed by Justin Starry
parent d4eb49d252
commit 0d8f3139ae
11 changed files with 332 additions and 58 deletions

View File

@ -2011,6 +2011,87 @@
"resolved": "https://registry.npmjs.org/@csstools/normalize.css/-/normalize.css-10.1.0.tgz",
"integrity": "sha512-ij4wRiunFfaJxjB0BdrYHIH8FxBJpOwNPhhAcunlmPdXudL1WQV1qoP9un6JsEBAgQH+7UXyyjh0g7jTxXK6tg=="
},
"@emotion/cache": {
"version": "10.0.29",
"resolved": "https://registry.npmjs.org/@emotion/cache/-/cache-10.0.29.tgz",
"integrity": "sha512-fU2VtSVlHiF27empSbxi1O2JFdNWZO+2NFHfwO0pxgTep6Xa3uGb+3pVKfLww2l/IBGLNEZl5Xf/++A4wAYDYQ==",
"requires": {
"@emotion/sheet": "0.9.4",
"@emotion/stylis": "0.8.5",
"@emotion/utils": "0.11.3",
"@emotion/weak-memoize": "0.2.5"
}
},
"@emotion/core": {
"version": "10.0.28",
"resolved": "https://registry.npmjs.org/@emotion/core/-/core-10.0.28.tgz",
"integrity": "sha512-pH8UueKYO5jgg0Iq+AmCLxBsvuGtvlmiDCOuv8fGNYn3cowFpLN98L8zO56U0H1PjDIyAlXymgL3Wu7u7v6hbA==",
"requires": {
"@babel/runtime": "^7.5.5",
"@emotion/cache": "^10.0.27",
"@emotion/css": "^10.0.27",
"@emotion/serialize": "^0.11.15",
"@emotion/sheet": "0.9.4",
"@emotion/utils": "0.11.3"
}
},
"@emotion/css": {
"version": "10.0.27",
"resolved": "https://registry.npmjs.org/@emotion/css/-/css-10.0.27.tgz",
"integrity": "sha512-6wZjsvYeBhyZQYNrGoR5yPMYbMBNEnanDrqmsqS1mzDm1cOTu12shvl2j4QHNS36UaTE0USIJawCH9C8oW34Zw==",
"requires": {
"@emotion/serialize": "^0.11.15",
"@emotion/utils": "0.11.3",
"babel-plugin-emotion": "^10.0.27"
}
},
"@emotion/hash": {
"version": "0.8.0",
"resolved": "https://registry.npmjs.org/@emotion/hash/-/hash-0.8.0.tgz",
"integrity": "sha512-kBJtf7PH6aWwZ6fka3zQ0p6SBYzx4fl1LoZXE2RrnYST9Xljm7WfKJrU4g/Xr3Beg72MLrp1AWNUmuYJTL7Cow=="
},
"@emotion/memoize": {
"version": "0.7.4",
"resolved": "https://registry.npmjs.org/@emotion/memoize/-/memoize-0.7.4.tgz",
"integrity": "sha512-Ja/Vfqe3HpuzRsG1oBtWTHk2PGZ7GR+2Vz5iYGelAw8dx32K0y7PjVuxK6z1nMpZOqAFsRUPCkK1YjJ56qJlgw=="
},
"@emotion/serialize": {
"version": "0.11.16",
"resolved": "https://registry.npmjs.org/@emotion/serialize/-/serialize-0.11.16.tgz",
"integrity": "sha512-G3J4o8by0VRrO+PFeSc3js2myYNOXVJ3Ya+RGVxnshRYgsvErfAOglKAiy1Eo1vhzxqtUvjCyS5gtewzkmvSSg==",
"requires": {
"@emotion/hash": "0.8.0",
"@emotion/memoize": "0.7.4",
"@emotion/unitless": "0.7.5",
"@emotion/utils": "0.11.3",
"csstype": "^2.5.7"
}
},
"@emotion/sheet": {
"version": "0.9.4",
"resolved": "https://registry.npmjs.org/@emotion/sheet/-/sheet-0.9.4.tgz",
"integrity": "sha512-zM9PFmgVSqBw4zL101Q0HrBVTGmpAxFZH/pYx/cjJT5advXguvcgjHFTCaIO3enL/xr89vK2bh0Mfyj9aa0ANA=="
},
"@emotion/stylis": {
"version": "0.8.5",
"resolved": "https://registry.npmjs.org/@emotion/stylis/-/stylis-0.8.5.tgz",
"integrity": "sha512-h6KtPihKFn3T9fuIrwvXXUOwlx3rfUvfZIcP5a6rh8Y7zjE3O06hT5Ss4S/YI1AYhuZ1kjaE/5EaOOI2NqSylQ=="
},
"@emotion/unitless": {
"version": "0.7.5",
"resolved": "https://registry.npmjs.org/@emotion/unitless/-/unitless-0.7.5.tgz",
"integrity": "sha512-OWORNpfjMsSSUBVrRBVGECkhWcULOAJz9ZW8uK9qgxD+87M7jHRcvh/A96XXNhXTLmKcoYSQtBEX7lHMO7YRwg=="
},
"@emotion/utils": {
"version": "0.11.3",
"resolved": "https://registry.npmjs.org/@emotion/utils/-/utils-0.11.3.tgz",
"integrity": "sha512-0o4l6pZC+hI88+bzuaX/6BgOvQVhbt2PfmxauVaYOGgbsAw14wdKyvMCZXnsnsHys94iadcF+RG/wZyx6+ZZBw=="
},
"@emotion/weak-memoize": {
"version": "0.2.5",
"resolved": "https://registry.npmjs.org/@emotion/weak-memoize/-/weak-memoize-0.2.5.tgz",
"integrity": "sha512-6U71C2Wp7r5XtFtQzYrW5iKFT67OixrSxjI4MptCHzdSVlgabczzqLe0ZSgnub/5Kp4hSbpDB1tMytZY9pwxxA=="
},
"@hapi/address": {
"version": "2.1.4",
"resolved": "https://registry.npmjs.org/@hapi/address/-/address-2.1.4.tgz",
@ -3026,6 +3107,24 @@
"@types/react-router": "*"
}
},
"@types/react-select": {
"version": "3.0.15",
"resolved": "https://registry.npmjs.org/@types/react-select/-/react-select-3.0.15.tgz",
"integrity": "sha512-yPmkr6zgVFR95JqBtkVaVDK/u1jdbTw8c8n9h5zWY/481IoBKZgrvOHweJXGvnOJ43umH7ImcT5m/uj5uESMBQ==",
"requires": {
"@types/react": "*",
"@types/react-dom": "*",
"@types/react-transition-group": "*"
}
},
"@types/react-transition-group": {
"version": "4.4.0",
"resolved": "https://registry.npmjs.org/@types/react-transition-group/-/react-transition-group-4.4.0.tgz",
"integrity": "sha512-/QfLHGpu+2fQOqQaXh8MG9q03bFENooTb/it4jr5kKaZlDQfWvjqWZg48AwzPVMBHlRuTRAY7hRHCEOXz5kV6w==",
"requires": {
"@types/react": "*"
}
},
"@types/socket.io-client": {
"version": "1.4.33",
"resolved": "https://registry.npmjs.org/@types/socket.io-client/-/socket.io-client-1.4.33.tgz",
@ -3871,6 +3970,30 @@
"object.assign": "^4.1.0"
}
},
"babel-plugin-emotion": {
"version": "10.0.33",
"resolved": "https://registry.npmjs.org/babel-plugin-emotion/-/babel-plugin-emotion-10.0.33.tgz",
"integrity": "sha512-bxZbTTGz0AJQDHm8k6Rf3RQJ8tX2scsfsRyKVgAbiUPUNIRtlK+7JxP+TAd1kRLABFxe0CFm2VdK4ePkoA9FxQ==",
"requires": {
"@babel/helper-module-imports": "^7.0.0",
"@emotion/hash": "0.8.0",
"@emotion/memoize": "0.7.4",
"@emotion/serialize": "^0.11.16",
"babel-plugin-macros": "^2.0.0",
"babel-plugin-syntax-jsx": "^6.18.0",
"convert-source-map": "^1.5.0",
"escape-string-regexp": "^1.0.5",
"find-root": "^1.1.0",
"source-map": "^0.5.7"
},
"dependencies": {
"source-map": {
"version": "0.5.7",
"resolved": "https://registry.npmjs.org/source-map/-/source-map-0.5.7.tgz",
"integrity": "sha1-igOdLRAh0i0eoUyA2OpGi6LvP8w="
}
}
},
"babel-plugin-istanbul": {
"version": "5.2.0",
"resolved": "https://registry.npmjs.org/babel-plugin-istanbul/-/babel-plugin-istanbul-5.2.0.tgz",
@ -3994,6 +4117,11 @@
"resolved": "https://registry.npmjs.org/babel-plugin-named-asset-import/-/babel-plugin-named-asset-import-0.3.6.tgz",
"integrity": "sha512-1aGDUfL1qOOIoqk9QKGIo2lANk+C7ko/fqH0uIyC71x3PEGz0uVP8ISgfEsFuG+FKmjHTvFK/nNM8dowpmUxLA=="
},
"babel-plugin-syntax-jsx": {
"version": "6.18.0",
"resolved": "https://registry.npmjs.org/babel-plugin-syntax-jsx/-/babel-plugin-syntax-jsx-6.18.0.tgz",
"integrity": "sha1-CvMqmm4Tyno/1QaeYtew9Y0NiUY="
},
"babel-plugin-syntax-object-rest-spread": {
"version": "6.13.0",
"resolved": "https://registry.npmjs.org/babel-plugin-syntax-object-rest-spread/-/babel-plugin-syntax-object-rest-spread-6.13.0.tgz",
@ -6224,6 +6352,15 @@
"utila": "~0.4"
}
},
"dom-helpers": {
"version": "5.1.4",
"resolved": "https://registry.npmjs.org/dom-helpers/-/dom-helpers-5.1.4.tgz",
"integrity": "sha512-TjMyeVUvNEnOnhzs6uAn9Ya47GmMo3qq7m+Lr/3ON0Rs5kHvb8I+SQYjLUSYn7qhEm0QjW0yrBkvz9yOrwwz1A==",
"requires": {
"@babel/runtime": "^7.8.7",
"csstype": "^2.6.7"
}
},
"dom-serializer": {
"version": "0.2.2",
"resolved": "https://registry.npmjs.org/dom-serializer/-/dom-serializer-0.2.2.tgz",
@ -7503,6 +7640,11 @@
"pkg-dir": "^3.0.0"
}
},
"find-root": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/find-root/-/find-root-1.1.0.tgz",
"integrity": "sha512-NKfW6bec6GfKc0SGx1e07QZY9PE99u0Bft/0rzSD5k3sO/vwkVUpDUKVm5Gpp5Ue3YfShPFTX2070tDs5kB9Ng=="
},
"find-up": {
"version": "1.1.2",
"resolved": "https://registry.npmjs.org/find-up/-/find-up-1.1.2.tgz",
@ -10292,6 +10434,11 @@
"p-is-promise": "^2.0.0"
}
},
"memoize-one": {
"version": "5.1.1",
"resolved": "https://registry.npmjs.org/memoize-one/-/memoize-one-5.1.1.tgz",
"integrity": "sha512-HKeeBpWvqiVJD57ZUAsJNm71eHTykffzcLZVYWiVfQeI1rJtuEaS7hQiEpWfVVk18donPwJEcFKIkCmPJNOhHA=="
},
"memory-fs": {
"version": "0.4.1",
"resolved": "https://registry.npmjs.org/memory-fs/-/memory-fs-0.4.1.tgz",
@ -13231,6 +13378,14 @@
"resolved": "https://registry.npmjs.org/react-error-overlay/-/react-error-overlay-6.0.7.tgz",
"integrity": "sha512-TAv1KJFh3RhqxNvhzxj6LeT5NWklP6rDr2a0jaTfsZ5wSZWHOGeqQyejUp3xxLfPt2UpyJEcVQB/zyPcmonNFA=="
},
"react-input-autosize": {
"version": "2.2.2",
"resolved": "https://registry.npmjs.org/react-input-autosize/-/react-input-autosize-2.2.2.tgz",
"integrity": "sha512-jQJgYCA3S0j+cuOwzuCd1OjmBmnZLdqQdiLKRYrsMMzbjUrVDS5RvJUDwJqA7sKuksDuzFtm6hZGKFu7Mjk5aw==",
"requires": {
"prop-types": "^15.5.8"
}
},
"react-is": {
"version": "16.13.0",
"resolved": "https://registry.npmjs.org/react-is/-/react-is-16.13.0.tgz",
@ -13543,6 +13698,32 @@
}
}
},
"react-select": {
"version": "3.1.0",
"resolved": "https://registry.npmjs.org/react-select/-/react-select-3.1.0.tgz",
"integrity": "sha512-wBFVblBH1iuCBprtpyGtd1dGMadsG36W5/t2Aj8OE6WbByDg5jIFyT7X5gT+l0qmT5TqWhxX+VsKJvCEl2uL9g==",
"requires": {
"@babel/runtime": "^7.4.4",
"@emotion/cache": "^10.0.9",
"@emotion/core": "^10.0.9",
"@emotion/css": "^10.0.9",
"memoize-one": "^5.0.0",
"prop-types": "^15.6.0",
"react-input-autosize": "^2.2.2",
"react-transition-group": "^4.3.0"
}
},
"react-transition-group": {
"version": "4.4.1",
"resolved": "https://registry.npmjs.org/react-transition-group/-/react-transition-group-4.4.1.tgz",
"integrity": "sha512-Djqr7OQ2aPUiYurhPalTrVy9ddmFCCzwhqQmtN+J3+3DzLO209Fdr70QrN8Z3DsglWql6iY1lDWAfpFiBtuKGw==",
"requires": {
"@babel/runtime": "^7.5.5",
"dom-helpers": "^5.0.1",
"loose-envify": "^1.4.0",
"prop-types": "^15.6.2"
}
},
"read-pkg": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/read-pkg/-/read-pkg-1.1.0.tgz",

View File

@ -14,6 +14,7 @@
"@types/react": "^16.9.43",
"@types/react-dom": "^16.9.8",
"@types/react-router-dom": "^5.1.5",
"@types/react-select": "^3.0.15",
"@types/socket.io-client": "^1.4.33",
"bootstrap": "^4.5.0",
"bs58": "^4.0.1",
@ -26,6 +27,7 @@
"react-dom": "^16.13.1",
"react-router-dom": "^5.2.0",
"react-scripts": "3.4.1",
"react-select": "^3.1.0",
"socket.io-client": "^2.3.0",
"solana-sdk-wasm": "file:wasm/pkg",
"superstruct": "^0.10.12",

View File

@ -12,6 +12,7 @@ import StatsCard from "components/StatsCard";
import MessageBanner from "components/MessageBanner";
import Navbar from "components/Navbar";
import { ClusterStatusBanner } from "components/ClusterStatusButton";
import { SearchBar } from "components/SearchBar";
function App() {
return (
@ -21,6 +22,7 @@ function App() {
<Navbar />
<MessageBanner />
<ClusterStatusBanner />
<SearchBar />
<Switch>
<Route exact path={["/supply", "/accounts", "accounts/top"]}>
<div className="container mt-4">

View File

@ -1,7 +1,6 @@
import React from "react";
import { Link } from "react-router-dom";
import { PublicKey, StakeProgram } from "@solana/web3.js";
import { useHistory, useLocation } from "react-router-dom";
import {
FetchStatus,
useFetchAccountInfo,
@ -21,9 +20,6 @@ import { useFetchAccountHistory } from "providers/accounts/history";
type Props = { address: string };
export default function AccountDetails({ address }: Props) {
const fetchAccount = useFetchAccountInfo();
const [search, setSearch] = React.useState(address);
const history = useHistory();
const location = useLocation();
let pubkey: PublicKey | undefined;
try {
@ -33,35 +29,17 @@ export default function AccountDetails({ address }: Props) {
// TODO handle bad addresses
}
const updateAddress = () => {
history.push({ ...location, pathname: "/account/" + search });
};
// Fetch account on load
React.useEffect(() => {
setSearch(address);
if (pubkey) fetchAccount(pubkey);
}, [address]); // 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" && updateAddress()}
className="form-control form-control-prepended search text-monospace"
placeholder="Search for address"
/>
);
return (
<div className="container">
<div className="container mt-n3">
<div className="header">
<div className="header-body">
<h6 className="header-pretitle">Address</h6>
<h4 className="header-title text-monospace text-truncate font-weight-bold">
{address}
</h4>
<h6 className="header-pretitle">Details</h6>
<h4 className="header-title">Account</h4>
</div>
</div>
{pubkey && <AccountCards pubkey={pubkey} />}
@ -100,10 +78,18 @@ function UnknownAccountCard({ account }: { account: Account }) {
return (
<div className="card">
<div className="card-header align-items-center">
<h3 className="card-header-title">Account Overview</h3>
<h3 className="card-header-title">Overview</h3>
</div>
<TableCardBody>
<tr>
<td>Address</td>
<td className="text-right">
<Copyable text={account.pubkey.toBase58()} right bottom>
<code>{displayAddress(account.pubkey.toBase58())}</code>
</Copyable>
</td>
</tr>
<tr>
<td>Balance (SOL)</td>
<td className="text-right text-uppercase">

View File

@ -1,17 +1,9 @@
import React from "react";
import Logo from "img/logos-solana/light-explorer-logo.svg";
import { Location } from "history";
import { pickCluster } from "utils/url";
import { clusterPath } 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);
@ -31,11 +23,15 @@ export default function Navbar() {
<span className="navbar-toggler-icon"></span>
</button>
<div className={`collapse navbar-collapse ml-auto mr-4 ${collapse ? "show" : ""}`}>
<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
Cluster Stats
</NavLink>
</li>
<li className="nav-item">

View File

@ -0,0 +1,92 @@
import React from "react";
import bs58 from "bs58";
import { useHistory, useLocation } from "react-router-dom";
import Select, { InputActionMeta, ActionMeta, ValueType } from "react-select";
import StateManager from "react-select";
export function SearchBar() {
const [search, setSearch] = React.useState("");
const selectRef = React.useRef<StateManager<any> | null>(null);
const history = useHistory();
const location = useLocation();
const onChange = (
{ value: pathname }: ValueType<any>,
meta: ActionMeta<any>
) => {
if (meta.action === "select-option") {
history.push({ ...location, pathname });
setSearch("");
}
};
const onInputChange = (value: string, { action }: InputActionMeta) => {
if (action === "input-change") setSearch(value);
};
const options = ((searchValue: string) => {
try {
const decoded = bs58.decode(searchValue);
if (decoded.length === 32) {
return [
{
label: "Account",
options: [
{
label: searchValue,
value: "/address/" + searchValue,
},
],
},
];
} else if (decoded.length === 64) {
return [
{
label: "Transaction",
options: [
{
label: searchValue,
value: "/tx/" + searchValue,
},
],
},
];
}
} catch (err) {}
return [];
})(search);
const resetValue = "" as any;
return (
<div className="container my-4">
<div className="row align-items-center">
<div className="col">
<Select
ref={(ref) => (selectRef.current = ref)}
options={options}
noOptionsMessage={() => "No Results"}
placeholder="Search by address or signature"
value={resetValue}
inputValue={search}
blurInputOnSelect
onMenuClose={() => selectRef.current?.blur()}
onChange={onChange}
onInputChange={onInputChange}
components={{ DropdownIndicator }}
styles={{
control: (styles) => ({ ...styles, borderRadius: "7px" }),
}}
/>
</div>
</div>
</div>
);
}
function DropdownIndicator() {
return (
<div className="search-indicator">
<span className="fe fe-search"></span>
</div>
);
}

View File

@ -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 Status</h4>
<h4 className="card-header-title">Live Cluster Stats</h4>
</div>
</div>
</div>

View File

@ -36,13 +36,11 @@ export default function TransactionDetails({ signature }: Props) {
}, [signature]); // eslint-disable-line react-hooks/exhaustive-deps
return (
<div className="container">
<div className="container mt-n3">
<div className="header">
<div className="header-body">
<h6 className="header-pretitle">Transaction</h6>
<h4 className="header-title text-monospace text-truncate font-weight-bold">
{signature}
</h4>
<h6 className="header-pretitle">Details</h6>
<h4 className="header-title">Transaction</h4>
</div>
</div>
@ -103,7 +101,7 @@ function StatusCard({ signature }: Props) {
return (
<div className="card">
<div className="card-header align-items-center">
<h3 className="card-header-title">Status</h3>
<h3 className="card-header-title">Overview</h3>
<button
className="btn btn-white btn-sm"
onClick={() => refresh(signature)}
@ -114,6 +112,15 @@ function StatusCard({ signature }: Props) {
</div>
<TableCardBody>
<tr>
<td>Signature</td>
<td className="text-right">
<Copyable text={signature} right bottom>
<code>{signature}</code>
</Copyable>
</td>
</tr>
<tr>
<td>Result</td>
<td className="text-right">{renderResult()}</td>

View File

@ -1,15 +1,7 @@
import React from "react";
import io from "socket.io-client";
import {
object,
number,
is,
StructType,
array,
nullable,
any,
} from "superstruct";
import { object, number, is, StructType, any } from "superstruct";
import { useCluster, Cluster } from "providers/cluster";
// TODO: use `partial` when it is fixed
@ -51,11 +43,7 @@ export const PERF_UPDATE_SEC = 5;
// https://github.com/ianstormtaylor/superstruct/issues/405
const PerformanceInfo = object({
avgTPS: number(),
perfHistory: object({
s: array(nullable(number())),
m: array(nullable(number())),
l: array(nullable(number())),
}),
perfHistory: any(),
totalTransactionCount: number(),
});

View File

@ -150,3 +150,16 @@ h4.slot-pill {
.line-height-md {
line-height: 1.5rem;
}
.search-indicator {
color: hsl(0,0%,60%);
display: flex;
padding: 8px 10px;
transition: color 150ms;
box-sizing: border-box;
cursor: pointer;
&:hover {
color: hsl(0,0%,40%);
}
}

View File

@ -5,6 +5,13 @@ export function useQuery() {
return new URLSearchParams(useLocation().search);
}
export const clusterPath = (pathname: string) => {
return (location: Location) => ({
...pickCluster(location),
pathname,
});
};
export function pickCluster(location: Location): Location {
const cluster = new URLSearchParams(location.search).get("cluster");