diff --git a/explorer/package-lock.json b/explorer/package-lock.json index d07a2d0153..c5f21e9916 100644 --- a/explorer/package-lock.json +++ b/explorer/package-lock.json @@ -1645,6 +1645,11 @@ "@types/node": "*" } }, + "@types/history": { + "version": "4.7.5", + "resolved": "https://registry.npmjs.org/@types/history/-/history-4.7.5.tgz", + "integrity": "sha512-wLD/Aq2VggCJXSjxEwrMafIP51Z+13H78nXIX0ABEuIGhmB5sNGbR113MOKo+yfw+RDo1ZU3DM6yfnnRF/+ouw==" + }, "@types/istanbul-lib-coverage": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/@types/istanbul-lib-coverage/-/istanbul-lib-coverage-2.0.1.tgz", @@ -1732,6 +1737,25 @@ "@types/react": "*" } }, + "@types/react-router": { + "version": "5.1.5", + "resolved": "https://registry.npmjs.org/@types/react-router/-/react-router-5.1.5.tgz", + "integrity": "sha512-RZPdCtZympi6X7EkGyaU7ISiAujDYTWgqMF9owE3P6efITw27IWQykcti0BvA5h4Mu1LLl5rxrpO3r8kHyUZ/Q==", + "requires": { + "@types/history": "*", + "@types/react": "*" + } + }, + "@types/react-router-dom": { + "version": "5.1.3", + "resolved": "https://registry.npmjs.org/@types/react-router-dom/-/react-router-dom-5.1.3.tgz", + "integrity": "sha512-pCq7AkOvjE65jkGS5fQwQhvUp4+4PVD9g39gXLZViP2UqFiFzsEpB3PKf0O6mdbKsewSK8N14/eegisa/0CwnA==", + "requires": { + "@types/history": "*", + "@types/react": "*", + "@types/react-router": "*" + } + }, "@types/stack-utils": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/@types/stack-utils/-/stack-utils-1.0.1.tgz", @@ -6212,6 +6236,11 @@ "resolved": "https://registry.npmjs.org/growly/-/growly-1.3.0.tgz", "integrity": "sha1-8QdIy+dq+WS3yWyTxrzCivEgwIE=" }, + "gud": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/gud/-/gud-1.0.0.tgz", + "integrity": "sha512-zGEOVKFM5sVPPrYs7J5/hYEw2Pof8KCyOwyhG8sAF26mCAeUFAcYPu1mwB7hhpIP29zOIBaDqwuHdLp0jvZXjw==" + }, "gzip-size": { "version": "5.1.1", "resolved": "https://registry.npmjs.org/gzip-size/-/gzip-size-5.1.1.tgz", @@ -6347,6 +6376,19 @@ "resolved": "https://registry.npmjs.org/hex-color-regex/-/hex-color-regex-1.1.0.tgz", "integrity": "sha512-l9sfDFsuqtOqKDsQdqrMRk0U85RZc0RtOR9yPI7mRVOa4FsR/BVnZ0shmQRM96Ji99kYZP/7hn1cedc1+ApsTQ==" }, + "history": { + "version": "4.10.1", + "resolved": "https://registry.npmjs.org/history/-/history-4.10.1.tgz", + "integrity": "sha512-36nwAD620w12kuzPAsyINPWJqlNbij+hpK1k9XRloDtym8mxzGYl2c17LnV6IAGB2Dmg4tEa7G7DlawS0+qjew==", + "requires": { + "@babel/runtime": "^7.1.2", + "loose-envify": "^1.2.0", + "resolve-pathname": "^3.0.0", + "tiny-invariant": "^1.0.2", + "tiny-warning": "^1.0.0", + "value-equal": "^1.0.1" + } + }, "hmac-drbg": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/hmac-drbg/-/hmac-drbg-1.0.1.tgz", @@ -6357,6 +6399,14 @@ "minimalistic-crypto-utils": "^1.0.1" } }, + "hoist-non-react-statics": { + "version": "3.3.2", + "resolved": "https://registry.npmjs.org/hoist-non-react-statics/-/hoist-non-react-statics-3.3.2.tgz", + "integrity": "sha512-/gGivxi8JPKWNm/W0jSmzcMPpfpPLc3dY/6GxhX2hQ9iGj3aDfklV4ET7NjKpSinLpJ5vafa9iiGIEZg10SfBw==", + "requires": { + "react-is": "^16.7.0" + } + }, "hosted-git-info": { "version": "2.8.8", "resolved": "https://registry.npmjs.org/hosted-git-info/-/hosted-git-info-2.8.8.tgz", @@ -9122,6 +9172,16 @@ "resolved": "https://registry.npmjs.org/min-indent/-/min-indent-1.0.0.tgz", "integrity": "sha1-z8RcN+nsDY8KDsPdTvf3w6vjklY=" }, + "mini-create-react-context": { + "version": "0.3.2", + "resolved": "https://registry.npmjs.org/mini-create-react-context/-/mini-create-react-context-0.3.2.tgz", + "integrity": "sha512-2v+OeetEyliMt5VHMXsBhABoJ0/M4RCe7fatd/fBy6SMiKazUSEt3gxxypfnk2SHMkdBYvorHRoQxuGoiwbzAw==", + "requires": { + "@babel/runtime": "^7.4.0", + "gud": "^1.0.0", + "tiny-warning": "^1.0.2" + } + }, "mini-css-extract-plugin": { "version": "0.9.0", "resolved": "https://registry.npmjs.org/mini-css-extract-plugin/-/mini-css-extract-plugin-0.9.0.tgz", @@ -11702,6 +11762,52 @@ "resolved": "https://registry.npmjs.org/react-is/-/react-is-16.13.0.tgz", "integrity": "sha512-GFMtL0vHkiBv9HluwNZTggSn/sCyEt9n02aM0dSAjGGyqyNlAyftYm4phPxdvCigG15JreC5biwxCgTAJZ7yAA==" }, + "react-router": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/react-router/-/react-router-5.1.2.tgz", + "integrity": "sha512-yjEuMFy1ONK246B+rsa0cUam5OeAQ8pyclRDgpxuSCrAlJ1qN9uZ5IgyKC7gQg0w8OM50NXHEegPh/ks9YuR2A==", + "requires": { + "@babel/runtime": "^7.1.2", + "history": "^4.9.0", + "hoist-non-react-statics": "^3.1.0", + "loose-envify": "^1.3.1", + "mini-create-react-context": "^0.3.0", + "path-to-regexp": "^1.7.0", + "prop-types": "^15.6.2", + "react-is": "^16.6.0", + "tiny-invariant": "^1.0.2", + "tiny-warning": "^1.0.0" + }, + "dependencies": { + "isarray": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/isarray/-/isarray-0.0.1.tgz", + "integrity": "sha1-ihis/Kmo9Bd+Cav8YDiTmwXR7t8=" + }, + "path-to-regexp": { + "version": "1.8.0", + "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-1.8.0.tgz", + "integrity": "sha512-n43JRhlUKUAlibEJhPeir1ncUID16QnEjNpwzNdO3Lm4ywrBpBZ5oLD0I6br9evr1Y9JTqwRtAh7JLoOzAQdVA==", + "requires": { + "isarray": "0.0.1" + } + } + } + }, + "react-router-dom": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/react-router-dom/-/react-router-dom-5.1.2.tgz", + "integrity": "sha512-7BPHAaIwWpZS074UKaw1FjVdZBSVWEk8IuDXdB+OkLb8vd/WRQIpA4ag9WQk61aEfQs47wHyjWUoUGGZxpQXew==", + "requires": { + "@babel/runtime": "^7.1.2", + "history": "^4.9.0", + "loose-envify": "^1.3.1", + "prop-types": "^15.6.2", + "react-router": "5.1.2", + "tiny-invariant": "^1.0.2", + "tiny-warning": "^1.0.0" + } + }, "react-scripts": { "version": "3.4.0", "resolved": "https://registry.npmjs.org/react-scripts/-/react-scripts-3.4.0.tgz", @@ -12084,6 +12190,11 @@ "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-3.0.0.tgz", "integrity": "sha1-six699nWiBvItuZTM17rywoYh0g=" }, + "resolve-pathname": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/resolve-pathname/-/resolve-pathname-3.0.0.tgz", + "integrity": "sha512-C7rARubxI8bXFNB/hqcp/4iUeIXJhJZvFPFPiSPRnhU5UPxzMFIl+2E6yY6c4k9giDJAhtV+enfA+G89N6Csng==" + }, "resolve-url": { "version": "0.2.1", "resolved": "https://registry.npmjs.org/resolve-url/-/resolve-url-0.2.1.tgz", @@ -13615,6 +13726,11 @@ "resolved": "https://registry.npmjs.org/tiny-invariant/-/tiny-invariant-1.1.0.tgz", "integrity": "sha512-ytxQvrb1cPc9WBEI/HSeYYoGD0kWnGEOR8RY6KomWLBVhqz0RgTwVO9dLrGz7dC+nN9llyI7OKAgRq8Vq4ZBSw==" }, + "tiny-warning": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/tiny-warning/-/tiny-warning-1.0.3.tgz", + "integrity": "sha512-lBN9zLN/oAf68o3zNXYrdCt1kP8WsiGW8Oo2ka41b2IM5JL/S1CTyX1rW0mb/zSuJun0ZUrDxx4sqvYS2FWzPA==" + }, "tmp": { "version": "0.0.33", "resolved": "https://registry.npmjs.org/tmp/-/tmp-0.0.33.tgz", @@ -14009,6 +14125,11 @@ "spdx-expression-parse": "^3.0.0" } }, + "value-equal": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/value-equal/-/value-equal-1.0.1.tgz", + "integrity": "sha512-NOJ6JZCAWr0zlxZt+xqCHNTEKOsrks2HQd4MqhP1qy4z1SkbEP467eNx6TgDKXMvUOb+OENfJCZwM+16n7fRfw==" + }, "vary": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz", diff --git a/explorer/package.json b/explorer/package.json index f67bc27b5a..b38c859eb2 100644 --- a/explorer/package.json +++ b/explorer/package.json @@ -12,12 +12,14 @@ "@types/node": "^12.0.0", "@types/react": "^16.9.0", "@types/react-dom": "^16.9.0", + "@types/react-router-dom": "^5.1.3", "bootstrap": "^4.4.1", "bs58": "^4.0.1", "node-sass": "^4.13.1", "prettier": "^1.19.1", "react": "^16.13.0", "react-dom": "^16.13.0", + "react-router-dom": "^5.1.2", "react-scripts": "3.4.0", "typescript": "^3.8.0" }, diff --git a/explorer/src/App.tsx b/explorer/src/App.tsx index e4a65e76f8..efaf166185 100644 --- a/explorer/src/App.tsx +++ b/explorer/src/App.tsx @@ -1,4 +1,5 @@ import React from "react"; +import { Link } from "react-router-dom"; import { ClusterProvider } from "./providers/cluster"; import { @@ -15,9 +16,11 @@ import TransactionsCard from "./components/TransactionsCard"; import ClusterModal from "./components/ClusterModal"; import TransactionModal from "./components/TransactionModal"; import Logo from "./img/logos-solana/light-explorer-logo.svg"; +import { useCurrentTab, Tab } from "./providers/tab"; function App() { const [showClusterModal, setShowClusterModal] = React.useState(false); + const currentTab = useCurrentTab(); return ( @@ -28,14 +31,39 @@ function App() { />
+ +
-
-
- Solana Explorer +
+
+ setShowClusterModal(true)} + />
-
+
+
+
+
    +
  • + +
  • +
  • + +
  • +
+
+
setShowClusterModal(true)} /> @@ -48,13 +76,13 @@ function App() {
- + {currentTab === "Transactions" ? : null}
- + {currentTab === "Accounts" ? : null}
@@ -70,6 +98,19 @@ function App() { ); } +function NavLink({ href, tab }: { href: string; tab: Tab }) { + let classes = "nav-link"; + if (tab === useCurrentTab()) { + classes += " active"; + } + + return ( + + {tab} + + ); +} + type OverlayProps = { show: boolean; onClick: () => void; diff --git a/explorer/src/components/ClusterStatusButton.tsx b/explorer/src/components/ClusterStatusButton.tsx index ab7013f388..a43ced8ee3 100644 --- a/explorer/src/components/ClusterStatusButton.tsx +++ b/explorer/src/components/ClusterStatusButton.tsx @@ -1,22 +1,41 @@ import React from "react"; import { useCluster, ClusterStatus, Cluster } from "../providers/cluster"; -function ClusterStatusButton({ onClick }: { onClick: () => void }) { +function ClusterStatusButton({ + onClick, + expand +}: { + onClick: () => void; + expand?: boolean; +}) { return (
-
); } -function Button() { +function Button({ expand }: { expand?: boolean }) { 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 lift btn-outline-${variant}`; + } + }; + + let spinnerClasses = "spinner-grow spinner-grow-sm mr-2"; + if (!expand) { + spinnerClasses += " text-warning"; + } + switch (status) { case ClusterStatus.Connected: return ( - + {statusName} @@ -24,9 +43,9 @@ function Button() { case ClusterStatus.Connecting: return ( - + @@ -36,7 +55,7 @@ function Button() { case ClusterStatus.Failure: return ( - + {statusName} diff --git a/explorer/src/index.tsx b/explorer/src/index.tsx index 6154e9544d..a8558fa017 100644 --- a/explorer/src/index.tsx +++ b/explorer/src/index.tsx @@ -1,10 +1,19 @@ import React from "react"; import ReactDOM from "react-dom"; +import { BrowserRouter as Router } from "react-router-dom"; import "./scss/theme.scss"; import App from "./App"; import * as serviceWorker from "./serviceWorker"; +import { TabProvider } from "./providers/tab"; -ReactDOM.render(, document.getElementById("root")); +ReactDOM.render( + + + + + , + document.getElementById("root") +); // If you want your app to work offline and load faster, you can change // unregister() to register() below. Note this comes with some pitfalls. diff --git a/explorer/src/providers/accounts.tsx b/explorer/src/providers/accounts.tsx index 7f20ef7465..cb0bda93b2 100644 --- a/explorer/src/providers/accounts.tsx +++ b/explorer/src/providers/accounts.tsx @@ -95,18 +95,19 @@ function reducer(state: State, action: Action): State { return state; } +export const ACCOUNT_PATHS = ["account", "accounts", "address", "addresses"]; + function urlAddresses(): Array { const addresses: Array = []; - return addresses - .concat(findGetParameter("account")?.split(",") || []) - .concat(findGetParameter("accounts")?.split(",") || []) - .concat(findPathSegment("account")?.split(",") || []) - .concat(findPathSegment("accounts")?.split(",") || []) - .concat(findGetParameter("address")?.split(",") || []) - .concat(findGetParameter("addresses")?.split(",") || []) - .concat(findPathSegment("address")?.split(",") || []) - .concat(findPathSegment("addresses")?.split(",") || []) - .filter(a => a.length > 0); + + ACCOUNT_PATHS.forEach(path => { + const params = findGetParameter(path)?.split(",") || []; + const segments = findPathSegment(path)?.split(",") || []; + addresses.push(...params); + addresses.push(...segments); + }); + + return addresses.filter(a => a.length > 0); } function initState(): State { diff --git a/explorer/src/providers/tab.tsx b/explorer/src/providers/tab.tsx new file mode 100644 index 0000000000..18f8b36942 --- /dev/null +++ b/explorer/src/providers/tab.tsx @@ -0,0 +1,27 @@ +import React from "react"; +import { useLocation } from "react-router-dom"; + +import { ACCOUNT_PATHS } from "./accounts"; + +export type Tab = "Transactions" | "Accounts"; +const StateContext = React.createContext(undefined); + +type TabProviderProps = { children: React.ReactNode }; +export function TabProvider({ children }: TabProviderProps) { + const location = useLocation(); + const paths = location.pathname.slice(1).split("/"); + let tab: Tab = "Transactions"; + if (ACCOUNT_PATHS.includes(paths[0].toLowerCase())) { + tab = "Accounts"; + } + + return {children}; +} + +export function useCurrentTab() { + const context = React.useContext(StateContext); + if (!context) { + throw new Error(`useCurrentTab must be used within a TabProvider`); + } + return context; +} diff --git a/explorer/src/providers/transactions.tsx b/explorer/src/providers/transactions.tsx index ba3ad770b8..73884a1435 100644 --- a/explorer/src/providers/transactions.tsx +++ b/explorer/src/providers/transactions.tsx @@ -131,20 +131,26 @@ function reducer(state: State, action: Action): State { return state; } +export const TX_PATHS = [ + "tx", + "txs", + "txn", + "txns", + "transaction", + "transactions" +]; + function urlSignatures(): Array { const signatures: Array = []; - return signatures - .concat(findGetParameter("tx")?.split(",") || []) - .concat(findGetParameter("txn")?.split(",") || []) - .concat(findGetParameter("txs")?.split(",") || []) - .concat(findGetParameter("txns")?.split(",") || []) - .concat(findGetParameter("transaction")?.split(",") || []) - .concat(findGetParameter("transactions")?.split(",") || []) - .concat(findPathSegment("tx")?.split(",") || []) - .concat(findPathSegment("txn")?.split(",") || []) - .concat(findPathSegment("transaction")?.split(",") || []) - .concat(findPathSegment("transactions")?.split(",") || []) - .filter(s => s.length > 0); + + TX_PATHS.forEach(path => { + const params = findGetParameter(path)?.split(",") || []; + const segments = findPathSegment(path)?.split(",") || []; + signatures.push(...params); + signatures.push(...segments); + }); + + return signatures.filter(s => s.length > 0); } function initState(): State { diff --git a/explorer/src/scss/_solana-variables.scss b/explorer/src/scss/_solana-variables.scss index fa4a9c886d..ad11ae3db1 100644 --- a/explorer/src/scss/_solana-variables.scss +++ b/explorer/src/scss/_solana-variables.scss @@ -31,7 +31,7 @@ $rainbow-3: #79abd2; $rainbow-4: #38d0bd; $rainbow-5: #1dd79b; -$primary: #00ffbd; +$primary: #65D39F; $primary-desat: #42ba96; $secondary: $gray-700; $success: #42ba96; diff --git a/explorer/src/utils.ts b/explorer/src/utils.ts index c84c8ff69b..3a4e9fa9d4 100644 --- a/explorer/src/utils.ts +++ b/explorer/src/utils.ts @@ -18,7 +18,7 @@ export function findGetParameter(parameterName: string): string | null { .split("&") .forEach(function(item) { tmp = item.split("="); - if (tmp[0] === parameterName) { + if (tmp[0].toLowerCase() === parameterName) { if (tmp.length === 2) { result = decodeURIComponent(tmp[1]); } else if (tmp.length === 1) {