Add network selector (#7)
This commit is contained in:
committed by
Michael Vines
parent
875aeaa53f
commit
03345e9005
@ -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;
|
||||||
|
145
explorer/src/components/NetworkModal.tsx
Normal file
145
explorer/src/components/NetworkModal.tsx
Normal 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}>
|
||||||
|
×
|
||||||
|
</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 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>
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -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() {
|
||||||
|
@ -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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
@ -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';
|
||||||
|
|
||||||
|
Reference in New Issue
Block a user