diff --git a/explorer/src/App.tsx b/explorer/src/App.tsx index c2c9b4dbe2a023..fc377df0c2bfa3 100644 --- a/explorer/src/App.tsx +++ b/explorer/src/App.tsx @@ -3,10 +3,13 @@ import { NetworkProvider } from "./providers/network"; import { TransactionsProvider } from "./providers/transactions"; import NetworkStatusButton from "./components/NetworkStatusButton"; import TransactionsCard from "./components/TransactionsCard"; +import NetworkModal from "./components/NetworkModal"; function App() { + const [showModal, setShowModal] = React.useState(false); return ( + setShowModal(false)} />
@@ -17,7 +20,7 @@ function App() {

Solana Explorer

- + setShowModal(true)} />
@@ -34,8 +37,21 @@ function App() { + + setShowModal(false)} />
); } +type OverlayProps = { + show: boolean; + onClick: () => void; +}; + +function Overlay({ show, onClick }: OverlayProps) { + return show ? ( +
+ ) : null; +} + export default App; diff --git a/explorer/src/components/NetworkModal.tsx b/explorer/src/components/NetworkModal.tsx new file mode 100644 index 00000000000000..0e53dea60f7c76 --- /dev/null +++ b/explorer/src/components/NetworkModal.tsx @@ -0,0 +1,145 @@ +import React from "react"; +import { + useNetwork, + useNetworkDispatch, + updateNetwork, + NetworkStatus, + networkUrl, + networkName, + NETWORKS, + Network +} from "../providers/network"; + +type Props = { + show: boolean; + onClose: () => void; +}; + +function NetworkModal({ show, onClose }: Props) { + const cancelClose = React.useCallback(e => e.stopPropagation(), []); + + return ( +
+
+
+
+ + × + + +

Explorer Settings

+ +

+ Preferences will not be saved (yet). +

+ +
+ +

Cluster

+ +

+ Connect to your preferred cluster. +

+ + +
+
+
+
+ ); +} + +type InputProps = { activeSuffix: string; active: boolean }; +function CustomNetworkInput({ activeSuffix, active }: InputProps) { + const { customUrl } = useNetwork(); + const dispatch = useNetworkDispatch(); + const [editing, setEditing] = React.useState(false); + + const customClass = (prefix: string) => + active ? `${prefix}-${activeSuffix}` : ""; + + const inputTextClass = editing ? "" : "text-muted"; + return ( +
updateNetwork(dispatch, Network.Custom, customUrl)} + > + setEditing(true)} + onBlur={() => setEditing(false)} + onInput={e => + updateNetwork(dispatch, Network.Custom, e.currentTarget.value) + } + /> +
+
+ Custom: +
+
+
+ ); +} + +function NetworkToggle() { + const { status, network, customUrl } = useNetwork(); + const dispatch = useNetworkDispatch(); + + let activeSuffix = ""; + switch (status) { + case NetworkStatus.Connected: + activeSuffix = "success"; + break; + case NetworkStatus.Connecting: + activeSuffix = "warning"; + break; + case NetworkStatus.Failure: + activeSuffix = "danger"; + break; + } + + return ( +
+ {NETWORKS.map((net, index) => { + const active = net === network; + if (net === Network.Custom) + return ( + + ); + + const btnClass = active + ? `btn-outline-${activeSuffix}` + : "btn-white text-dark"; + + return ( + + ); + })} +
+ ); +} + +export default NetworkModal; diff --git a/explorer/src/components/NetworkStatusButton.tsx b/explorer/src/components/NetworkStatusButton.tsx index 34869ad391c942..e4d69882c1637b 100644 --- a/explorer/src/components/NetworkStatusButton.tsx +++ b/explorer/src/components/NetworkStatusButton.tsx @@ -1,27 +1,47 @@ import React from "react"; -import { useNetwork, NetworkStatus } from "../providers/network"; +import { useNetwork, NetworkStatus, Network } from "../providers/network"; -function NetworkStatusButton() { - const { status, url } = useNetwork(); +function NetworkStatusButton({ onClick }: { onClick: () => void }) { + return ( +
+
+ ); +} + +function Button() { + const { status, network, name, customUrl } = useNetwork(); + const statusName = + network !== Network.Custom ? `${name} Cluster` : `${customUrl}`; switch (status) { case NetworkStatus.Connected: - return {url}; + return ( + + + {statusName} + + ); case NetworkStatus.Connecting: return ( - - {"Connecting "} + + {statusName} ); case NetworkStatus.Failure: - return Disconnected; + return ( + + + {statusName} + + ); } } diff --git a/explorer/src/providers/network.tsx b/explorer/src/providers/network.tsx index c6dd14b2925f12..a833a1bafb7a82 100644 --- a/explorer/src/providers/network.tsx +++ b/explorer/src/providers/network.tsx @@ -2,22 +2,56 @@ import React from "react"; import { testnetChannelEndpoint, Connection } from "@solana/web3.js"; import { findGetParameter } from "../utils"; -export const DEFAULT_URL = testnetChannelEndpoint("stable"); - export enum NetworkStatus { Connected, Connecting, Failure } +export enum Network { + MainnetBeta, + TdS, + Devnet, + Custom +} + +export const NETWORKS = [ + Network.MainnetBeta, + Network.TdS, + Network.Devnet, + Network.Custom +]; + +export function networkName(network: Network): string { + switch (network) { + case Network.MainnetBeta: + return "Mainnet Beta"; + case Network.TdS: + return "Tour de SOL"; + case Network.Devnet: + return "Devnet"; + case Network.Custom: + return "Custom"; + } +} + +export const MAINNET_BETA_URL = "http://34.82.103.142"; +export const TDS_URL = "http://35.233.128.214"; +export const DEVNET_URL = testnetChannelEndpoint("stable"); + +export const DEFAULT_NETWORK = Network.MainnetBeta; +export const DEFAULT_CUSTOM_URL = "http://localhost:8899"; + interface State { - url: string; + network: Network; + customUrl: string; status: NetworkStatus; } interface Connecting { status: NetworkStatus.Connecting; - url: string; + network: Network; + customUrl: string; } interface Connected { @@ -38,15 +72,38 @@ function networkReducer(state: State, action: Action): State { return Object.assign({}, state, { status: action.status }); } case NetworkStatus.Connecting: { - return { url: action.url, status: action.status }; + return action; } } } -function initState(url: string): State { +function initState(): State { const networkUrlParam = findGetParameter("networkUrl"); + + let network; + let customUrl = DEFAULT_CUSTOM_URL; + switch (networkUrlParam) { + case null: + network = DEFAULT_NETWORK; + break; + case MAINNET_BETA_URL: + network = Network.MainnetBeta; + break; + case DEVNET_URL: + network = Network.Devnet; + break; + case TDS_URL: + network = Network.TdS; + break; + default: + network = Network.Custom; + customUrl = networkUrlParam || DEFAULT_CUSTOM_URL; + break; + } + return { - url: networkUrlParam || url, + network, + customUrl, status: NetworkStatus.Connecting }; } @@ -58,13 +115,13 @@ type NetworkProviderProps = { children: React.ReactNode }; export function NetworkProvider({ children }: NetworkProviderProps) { const [state, dispatch] = React.useReducer( networkReducer, - DEFAULT_URL, + undefined, initState ); React.useEffect(() => { // Connect to network immediately - updateNetwork(dispatch, state.url); + updateNetwork(dispatch, state.network, state.customUrl); }, []); // eslint-disable-line react-hooks/exhaustive-deps return ( @@ -76,14 +133,32 @@ export function NetworkProvider({ children }: NetworkProviderProps) { ); } -export async function updateNetwork(dispatch: Dispatch, newUrl: string) { +export function networkUrl(network: Network, customUrl: string) { + switch (network) { + case Network.Devnet: + return DEVNET_URL; + case Network.MainnetBeta: + return MAINNET_BETA_URL; + case Network.TdS: + return TDS_URL; + case Network.Custom: + return customUrl; + } +} + +export async function updateNetwork( + dispatch: Dispatch, + network: Network, + customUrl: string +) { dispatch({ status: NetworkStatus.Connecting, - url: newUrl + network, + customUrl }); try { - const connection = new Connection(newUrl); + const connection = new Connection(networkUrl(network, customUrl)); await connection.getRecentBlockhash(); dispatch({ status: NetworkStatus.Connected }); } catch (error) { @@ -97,7 +172,11 @@ export function useNetwork() { if (!context) { throw new Error(`useNetwork must be used within a NetworkProvider`); } - return context; + return { + ...context, + url: networkUrl(context.network, context.customUrl), + name: networkName(context.network) + }; } export function useNetworkDispatch() { diff --git a/explorer/src/scss/_solana.scss b/explorer/src/scss/_solana.scss index 20ff90948ffa00..755645dc858f20 100644 --- a/explorer/src/scss/_solana.scss +++ b/explorer/src/scss/_solana.scss @@ -9,3 +9,17 @@ code { background-color: $gray-200; color: $black; } + +.modal.show { + display: block; +} + +.modal .close { + cursor: pointer; +} + +.btn-outline-warning:hover { + .spinner-grow { + color: $dark !important; + } +} diff --git a/explorer/src/scss/theme.scss b/explorer/src/scss/theme.scss index 6de9478d1fbe63..a3329561134e47 100644 --- a/explorer/src/scss/theme.scss +++ b/explorer/src/scss/theme.scss @@ -5,6 +5,9 @@ * to ensure cascade of styles. */ + // Icon font +@import "../fonts/feather/feather"; + // Bootstrap functions @import '~bootstrap/scss/functions.scss';