Add network selector (#7)

This commit is contained in:
Justin Starry
2020-03-19 18:18:58 +08:00
committed by Michael Vines
parent 875aeaa53f
commit 03345e9005
6 changed files with 299 additions and 22 deletions

View File

@ -3,10 +3,13 @@ import { NetworkProvider } from "./providers/network";
import { TransactionsProvider } from "./providers/transactions"; import { TransactionsProvider } from "./providers/transactions";
import NetworkStatusButton from "./components/NetworkStatusButton"; import NetworkStatusButton from "./components/NetworkStatusButton";
import TransactionsCard from "./components/TransactionsCard"; import TransactionsCard from "./components/TransactionsCard";
import NetworkModal from "./components/NetworkModal";
function App() { function App() {
const [showModal, setShowModal] = React.useState(false);
return ( return (
<NetworkProvider> <NetworkProvider>
<NetworkModal show={showModal} onClose={() => setShowModal(false)} />
<div className="main-content"> <div className="main-content">
<div className="header"> <div className="header">
<div className="container"> <div className="container">
@ -17,7 +20,7 @@ function App() {
<h1 className="header-title">Solana Explorer</h1> <h1 className="header-title">Solana Explorer</h1>
</div> </div>
<div className="col-auto"> <div className="col-auto">
<NetworkStatusButton /> <NetworkStatusButton onClick={() => setShowModal(true)} />
</div> </div>
</div> </div>
</div> </div>
@ -34,8 +37,21 @@ function App() {
</div> </div>
</div> </div>
</div> </div>
<Overlay show={showModal} onClick={() => setShowModal(false)} />
</NetworkProvider> </NetworkProvider>
); );
} }
type OverlayProps = {
show: boolean;
onClick: () => void;
};
function Overlay({ show, onClick }: OverlayProps) {
return show ? (
<div className="modal-backdrop fade show" onClick={onClick}></div>
) : null;
}
export default App; export default App;

View File

@ -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 (
<div
className={`modal fade fixed-right ${show ? "show" : ""}`}
tabIndex={-1}
onClick={onClose}
>
<div className="modal-dialog modal-dialog-vertical">
<div className="modal-content">
<div className="modal-body" onClick={cancelClose}>
<span className="close" onClick={onClose}>
&times;
</span>
<h2 className="text-center mb-2 mt-4">Explorer Settings</h2>
<p className="text-center mb-4">
Preferences will not be saved (yet).
</p>
<hr className="mb-4" />
<h4 className="mb-1">Cluster</h4>
<p className="small text-muted mb-3">
Connect to your preferred cluster.
</p>
<NetworkToggle />
</div>
</div>
</div>
</div>
);
}
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 (
<div
className="btn input-group input-group-merge p-0"
onClick={() => updateNetwork(dispatch, Network.Custom, customUrl)}
>
<input
type="text"
defaultValue={customUrl}
className={`form-control form-control-prepended ${inputTextClass} ${customClass(
"border"
)}`}
onFocus={() => setEditing(true)}
onBlur={() => setEditing(false)}
onInput={e =>
updateNetwork(dispatch, Network.Custom, e.currentTarget.value)
}
/>
<div className="input-group-prepend">
<div className={`input-group-text pr-0 ${customClass("border")}`}>
<span className={customClass("text") || "text-dark"}>Custom:</span>
</div>
</div>
</div>
);
}
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 (
<div className="btn-group-toggle d-flex flex-wrap mb-4">
{NETWORKS.map((net, index) => {
const active = net === network;
if (net === Network.Custom)
return (
<CustomNetworkInput
key={index}
activeSuffix={activeSuffix}
active={active}
/>
);
const btnClass = active
? `btn-outline-${activeSuffix}`
: "btn-white text-dark";
return (
<label
key={index}
className={`btn text-left col-12 mb-3 ${btnClass}`}
>
<input
type="radio"
checked={active}
onChange={() => updateNetwork(dispatch, net, customUrl)}
/>
{`${networkName(net)}: `}
<span className="text-muted">{networkUrl(net, customUrl)}</span>
</label>
);
})}
</div>
);
}
export default NetworkModal;

View File

@ -1,27 +1,47 @@
import React from "react"; import React from "react";
import { useNetwork, NetworkStatus } from "../providers/network"; import { useNetwork, NetworkStatus, Network } from "../providers/network";
function NetworkStatusButton() { function NetworkStatusButton({ onClick }: { onClick: () => void }) {
const { status, url } = useNetwork(); return (
<div onClick={onClick}>
<Button />
</div>
);
}
function Button() {
const { status, network, name, customUrl } = useNetwork();
const statusName =
network !== Network.Custom ? `${name} Cluster` : `${customUrl}`;
switch (status) { switch (status) {
case NetworkStatus.Connected: case NetworkStatus.Connected:
return <span className="btn btn-white lift">{url}</span>; return (
<span className="btn btn-outline-success lift">
<span className="fe fe-check-circle mr-2"></span>
{statusName}
</span>
);
case NetworkStatus.Connecting: case NetworkStatus.Connecting:
return ( return (
<span className="btn btn-warning lift"> <span className="btn btn-outline-warning lift">
{"Connecting "}
<span <span
className="spinner-grow spinner-grow-sm text-dark" className="spinner-grow spinner-grow-sm text-warning mr-2"
role="status" role="status"
aria-hidden="true" aria-hidden="true"
></span> ></span>
{statusName}
</span> </span>
); );
case NetworkStatus.Failure: case NetworkStatus.Failure:
return <span className="btn btn-danger lift">Disconnected</span>; return (
<span className="btn btn-outline-danger lift">
<span className="fe fe-alert-circle mr-2"></span>
{statusName}
</span>
);
} }
} }

View File

@ -2,22 +2,56 @@ import React from "react";
import { testnetChannelEndpoint, Connection } from "@solana/web3.js"; import { testnetChannelEndpoint, Connection } from "@solana/web3.js";
import { findGetParameter } from "../utils"; import { findGetParameter } from "../utils";
export const DEFAULT_URL = testnetChannelEndpoint("stable");
export enum NetworkStatus { export enum NetworkStatus {
Connected, Connected,
Connecting, Connecting,
Failure 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 { interface State {
url: string; network: Network;
customUrl: string;
status: NetworkStatus; status: NetworkStatus;
} }
interface Connecting { interface Connecting {
status: NetworkStatus.Connecting; status: NetworkStatus.Connecting;
url: string; network: Network;
customUrl: string;
} }
interface Connected { interface Connected {
@ -38,15 +72,38 @@ function networkReducer(state: State, action: Action): State {
return Object.assign({}, state, { status: action.status }); return Object.assign({}, state, { status: action.status });
} }
case NetworkStatus.Connecting: { case NetworkStatus.Connecting: {
return { url: action.url, status: action.status }; return action;
} }
} }
} }
function initState(url: string): State { function initState(): State {
const networkUrlParam = findGetParameter("networkUrl"); 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 { return {
url: networkUrlParam || url, network,
customUrl,
status: NetworkStatus.Connecting status: NetworkStatus.Connecting
}; };
} }
@ -58,13 +115,13 @@ type NetworkProviderProps = { children: React.ReactNode };
export function NetworkProvider({ children }: NetworkProviderProps) { export function NetworkProvider({ children }: NetworkProviderProps) {
const [state, dispatch] = React.useReducer( const [state, dispatch] = React.useReducer(
networkReducer, networkReducer,
DEFAULT_URL, undefined,
initState initState
); );
React.useEffect(() => { React.useEffect(() => {
// Connect to network immediately // Connect to network immediately
updateNetwork(dispatch, state.url); updateNetwork(dispatch, state.network, state.customUrl);
}, []); // eslint-disable-line react-hooks/exhaustive-deps }, []); // eslint-disable-line react-hooks/exhaustive-deps
return ( 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({ dispatch({
status: NetworkStatus.Connecting, status: NetworkStatus.Connecting,
url: newUrl network,
customUrl
}); });
try { try {
const connection = new Connection(newUrl); const connection = new Connection(networkUrl(network, customUrl));
await connection.getRecentBlockhash(); await connection.getRecentBlockhash();
dispatch({ status: NetworkStatus.Connected }); dispatch({ status: NetworkStatus.Connected });
} catch (error) { } catch (error) {
@ -97,7 +172,11 @@ export function useNetwork() {
if (!context) { if (!context) {
throw new Error(`useNetwork must be used within a NetworkProvider`); 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() { export function useNetworkDispatch() {

View File

@ -9,3 +9,17 @@ code {
background-color: $gray-200; background-color: $gray-200;
color: $black; color: $black;
} }
.modal.show {
display: block;
}
.modal .close {
cursor: pointer;
}
.btn-outline-warning:hover {
.spinner-grow {
color: $dark !important;
}
}

View File

@ -5,6 +5,9 @@
* to ensure cascade of styles. * to ensure cascade of styles.
*/ */
// Icon font
@import "../fonts/feather/feather";
// Bootstrap functions // Bootstrap functions
@import '~bootstrap/scss/functions.scss'; @import '~bootstrap/scss/functions.scss';