Add network selector (#7)
This commit is contained in:
parent
875aeaa53f
commit
03345e9005
|
@ -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;
|
||||
|
|
|
@ -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}>
|
||||
×
|
||||
</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;
|
|
@ -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>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -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() {
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -5,6 +5,9 @@
|
|||
* to ensure cascade of styles.
|
||||
*/
|
||||
|
||||
// Icon font
|
||||
@import "../fonts/feather/feather";
|
||||
|
||||
// Bootstrap functions
|
||||
@import '~bootstrap/scss/functions.scss';
|
||||
|
||||
|
|
Loading…
Reference in New Issue