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)
+ }
+ />
+
+
+ );
+}
+
+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';