From 0d8f3139ae88d3909876c9f4bff83639bf8d1219 Mon Sep 17 00:00:00 2001 From: Justin Starry Date: Sun, 2 Aug 2020 01:46:22 +0800 Subject: [PATCH] Add unified search bar to the explorer --- explorer/package-lock.json | 181 ++++++++++++++++++ explorer/package.json | 2 + explorer/src/App.tsx | 2 + explorer/src/components/AccountDetails.tsx | 38 ++-- explorer/src/components/Navbar.tsx | 18 +- explorer/src/components/SearchBar.tsx | 92 +++++++++ explorer/src/components/StatsCard.tsx | 2 +- .../src/components/TransactionDetails.tsx | 19 +- explorer/src/providers/stats/solanaBeach.tsx | 16 +- explorer/src/scss/_solana.scss | 13 ++ explorer/src/utils/url.ts | 7 + 11 files changed, 332 insertions(+), 58 deletions(-) create mode 100644 explorer/src/components/SearchBar.tsx diff --git a/explorer/package-lock.json b/explorer/package-lock.json index d8ccf025d9..4cf41c34d2 100644 --- a/explorer/package-lock.json +++ b/explorer/package-lock.json @@ -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", diff --git a/explorer/package.json b/explorer/package.json index 6ecd8ca509..bf1f165fd5 100644 --- a/explorer/package.json +++ b/explorer/package.json @@ -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", diff --git a/explorer/src/App.tsx b/explorer/src/App.tsx index 5612c91420..4c8da5c76e 100644 --- a/explorer/src/App.tsx +++ b/explorer/src/App.tsx @@ -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() { +
diff --git a/explorer/src/components/AccountDetails.tsx b/explorer/src/components/AccountDetails.tsx index ee8b56194c..90e99070bf 100644 --- a/explorer/src/components/AccountDetails.tsx +++ b/explorer/src/components/AccountDetails.tsx @@ -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 = ( - setSearch(e.target.value)} - onKeyUp={(e) => e.key === "Enter" && updateAddress()} - className="form-control form-control-prepended search text-monospace" - placeholder="Search for address" - /> - ); - return ( -
+
-
Address
-

- {address} -

+
Details
+

Account

{pubkey && } @@ -100,10 +78,18 @@ function UnknownAccountCard({ account }: { account: Account }) { return (
-

Account Overview

+

Overview

+ + Address + + + {displayAddress(account.pubkey.toBase58())} + + + Balance (SOL) diff --git a/explorer/src/components/Navbar.tsx b/explorer/src/components/Navbar.tsx index 92c22feb03..116a9e7658 100644 --- a/explorer/src/components/Navbar.tsx +++ b/explorer/src/components/Navbar.tsx @@ -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() { -
+
  • - Cluster Status + Cluster Stats
  • diff --git a/explorer/src/components/SearchBar.tsx b/explorer/src/components/SearchBar.tsx new file mode 100644 index 0000000000..7c0720f213 --- /dev/null +++ b/explorer/src/components/SearchBar.tsx @@ -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 | null>(null); + const history = useHistory(); + const location = useLocation(); + + const onChange = ( + { value: pathname }: ValueType, + meta: ActionMeta + ) => { + 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 ( +
    +
    +
    +