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 NetworkStatusButton from "./components/NetworkStatusButton";
import TransactionsCard from "./components/TransactionsCard";
import NetworkModal from "./components/NetworkModal";
function App() {
const [showModal, setShowModal] = React.useState(false);
return (
<NetworkProvider>
<NetworkModal show={showModal} onClose={() => setShowModal(false)} />
<div className="main-content">
<div className="header">
<div className="container">
@ -17,7 +20,7 @@ function App() {
<h1 className="header-title">Solana Explorer</h1>
</div>
<div className="col-auto">
<NetworkStatusButton />
<NetworkStatusButton onClick={() => setShowModal(true)} />
</div>
</div>
</div>
@ -34,8 +37,21 @@ function App() {
</div>
</div>
</div>
<Overlay show={showModal} onClick={() => setShowModal(false)} />
</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;

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 { useNetwork, NetworkStatus } from "../providers/network";
import { useNetwork, NetworkStatus, Network } from "../providers/network";
function NetworkStatusButton() {
const { status, url } = useNetwork();
function NetworkStatusButton({ onClick }: { onClick: () => void }) {
return (
<div onClick={onClick}>
<Button />
</div>
);
}
function Button() {
const { status, network, name, customUrl } = useNetwork();
const statusName =
network !== Network.Custom ? `${name} Cluster` : `${customUrl}`;
switch (status) {
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:
return (
<span className="btn btn-warning lift">
{"Connecting "}
<span className="btn btn-outline-warning lift">
<span
className="spinner-grow spinner-grow-sm text-dark"
className="spinner-grow spinner-grow-sm text-warning mr-2"
role="status"
aria-hidden="true"
></span>
{statusName}
</span>
);
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 { 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() {

View File

@ -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;
}
}

View File

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