diff --git a/package.json b/package.json index 08e20779..53a4bc4d 100644 --- a/package.json +++ b/package.json @@ -26,6 +26,7 @@ "google-map-react": "^1.1.4", "humps": "^2.0.1", "ip": "^1.1.5", + "localforage": "^1.7.3", "lodash": "^4.17.11", "mobx": "^5.11.0", "mobx-react-lite": "^1.4.1", diff --git a/src/App.js b/src/App.js index 4ae00bb3..662ad325 100755 --- a/src/App.js +++ b/src/App.js @@ -12,7 +12,7 @@ import {createBrowserHistory} from 'history'; import {CssBaseline} from '@material-ui/core'; import {sleep} from './sleep'; -import EndpointConfig from './EndpointConfig'; +import * as EndpointConfig from './EndpointConfig'; // v1 components import BxDataTable from './v1/BxDataTable'; import BxTransactionChart from './v1/BxTransactionChart'; @@ -33,8 +33,6 @@ import {stylesV2, themeV2} from './v2/ThemeV2'; const history = createBrowserHistory(); -const BLOCK_EXPLORER_API_BASE = EndpointConfig.BLOCK_EXPLORER_API_BASE; - const BxAppBarThemed = withStyles(stylesV1)(BxAppBar); const BxDialogThemed = withStyles(stylesV1)(BxDialog); const BxDialogTransactionsThemed = withStyles(stylesV1)(BxDialogTransactions); @@ -58,14 +56,7 @@ class App extends Component { constructor(props) { super(props); - this.ws = null; - - // unused (for now) - // this.connection = new Connection(EndpointConfig.BLOCK_EXPLORER_RPC_URL); - - this.state = { - enabled: true, - dialogOpen: false, + this.defaultState = { selectedValue: null, currentMatch: null, stateLoading: false, @@ -82,35 +73,20 @@ class App extends Component { transactions: [], blocks: [], }; - - const self = this; - - // update global info (once, on load) - this.getRemoteState( - 'globalStats', - `http:${BLOCK_EXPLORER_API_BASE}/global-stats`, - ); - - // update cluster info (once, on load) - this.getRemoteState( - 'clusterInfo', - `http:${BLOCK_EXPLORER_API_BASE}/cluster-info`, - null, - null, - self.parseClusterInfo, + this.state = Object.assign( + { + enabled: true, + dialogOpen: false, + ws: null, + }, + this.defaultState, ); - // update blocks (once, on load) - self.updateBlocks(); - - self.updateTxnStats(); setInterval(() => { - self.updateTxnStats(); + this.updateTxnStats(); }, 30000); - - self.updateTransactions(); setInterval(() => { - self.updateTransactions(); + this.updateTransactions(); }, 10000); } @@ -175,10 +151,7 @@ class App extends Component { } updateTxnStats() { - this.getRemoteState( - 'txnStats', - `http:${BLOCK_EXPLORER_API_BASE}/txn-stats`, - ); + this.getRemoteState('txnStats', `${EndpointConfig.getApiUrl()}txn-stats`); } updateBlocks() { @@ -202,7 +175,7 @@ class App extends Component { this.getRemoteState( 'blocks', - `http:${BLOCK_EXPLORER_API_BASE}/blk-timeline`, + `${EndpointConfig.getApiUrl()}blk-timeline`, blkFun, 10, ); @@ -221,7 +194,7 @@ class App extends Component { this.getRemoteState( 'transactions', - `http:${BLOCK_EXPLORER_API_BASE}/txn-timeline`, + `${EndpointConfig.getApiUrl()}txn-timeline`, txnFun, 10, ); @@ -276,40 +249,57 @@ class App extends Component { } }; - componentDidMount() { - const self = this; - - if (!self.ws) { - let ws = new RobustWebSocket(`ws:${BLOCK_EXPLORER_API_BASE}/`); + onEndpointChange() { + if (this.ws) { + this.ws.close(); + this.ws = null; + } - ws.addEventListener('open', function() { - ws.send(JSON.stringify({hello: 'world'})); - }); + this.getRemoteState( + 'globalStats', + `${EndpointConfig.getApiUrl()}global-stats`, + ); + this.getRemoteState( + 'clusterInfo', + `${EndpointConfig.getApiUrl()}cluster-info`, + null, + null, + this.parseClusterInfo, + ); + this.updateBlocks(); + this.updateTxnStats(); + this.updateTransactions(); - ws.addEventListener('message', function(event) { - if (!self.state.enabled) { - return; - } + const ws = new RobustWebSocket(EndpointConfig.getApiWebsocketUrl()); + ws.addEventListener('open', () => { + ws.send(JSON.stringify({hello: 'world'})); + }); - self.onMessage(JSON.parse(event.data)); - }); + ws.addEventListener('message', event => { + if (this.state.enabled) { + this.onMessage(JSON.parse(event.data)); + } + }); - self.ws = ws; - } + this.ws = ws; + } - if (!self.locationListener) { - let locationListener = this.handleLocationChange(); + componentDidMount() { + this.onEndpointChange(); + if (!this.locationListener) { + const locationListener = this.handleLocationChange(); history.listen(locationListener); locationListener(window.location); - self.locationListener = locationListener; + this.locationListener = locationListener; } } componentWillUnmount() { if (this.ws) { this.ws.close(); + this.ws = null; } } @@ -462,6 +452,12 @@ class App extends Component { }); }; + setEndpointName = event => { + EndpointConfig.setEndpointName(event.target.value); + this.onEndpointChange(); + this.updateStateAttributes(this.defaultState); + }; + handleSearch = () => event => { let value = event.target.value; event.target.value = ''; @@ -470,7 +466,7 @@ class App extends Component { return; } - let url = `${BLOCK_EXPLORER_API_BASE}/search/${value}`; + let url = `${EndpointConfig.getApiUrl()}search/${value}`; axios.get(url).then(response => { let result = response.data; @@ -485,19 +481,19 @@ class App extends Component { let url = null; if (type === 'txns-by-prgid') { - url = `${BLOCK_EXPLORER_API_BASE}/txns-by-prgid/${id}`; + url = `${EndpointConfig.getApiUrl()}txns-by-prgid/${id}`; } if (type === 'txn') { - url = `${BLOCK_EXPLORER_API_BASE}/txn/${id}`; + url = `${EndpointConfig.getApiUrl()}txn/${id}`; } if (type === 'ent') { - url = `${BLOCK_EXPLORER_API_BASE}/ent/${id}`; + url = `${EndpointConfig.getApiUrl()}ent/${id}`; } if (type === 'blk') { - url = `${BLOCK_EXPLORER_API_BASE}/blk/${id}`; + url = `${EndpointConfig.getApiUrl()}blk/${id}`; } return url; @@ -573,6 +569,7 @@ class App extends Component { handleSearch={self.handleSearch(self)} enabled={this.state.enabled} handleSwitch={this.toggleEnabled(self)} + handleSetEndpointName={this.setEndpointName} handleMap={this.showMap(self)} />
diff --git a/src/EndpointConfig.js b/src/EndpointConfig.js index 0401080c..5eef1eba 100644 --- a/src/EndpointConfig.js +++ b/src/EndpointConfig.js @@ -1,17 +1,100 @@ +import localforage from 'localforage'; +import {parse as urlParse, format as urlFormat} from 'url'; + +const urlMap = { + local: `http://${window.location.hostname}:8899`, + 'testnet-edge': 'http://edge.testnet.solana.com:8899', + 'testnet-beta': 'http://beta.testnet.solana.com:8899', + testnet: 'http://testnet.solana.com:8899', + /* + TODO: Switch to TLS endpoints... + 'testnet-edge': 'https://api.edge.testnet.solana.com', + 'testnet-beta': 'https://api.beta.testnet.solana.com', + testnet: 'https://api.testnet.solana.com', + */ +}; + +let endpointName = process.env.NODE_ENV === 'development' ? 'local' : 'testnet'; + +export async function load() { + try { + const newEndpointName = await localforage.getItem('endpointName'); + if (typeof urlMap[newEndpointName] === 'string') { + endpointName = newEndpointName; + } + } catch (err) { + console.log( + `Unable to load endpointName from localforage, using default of ${endpointName}: ${err}`, + ); + } + console.log('EndpointConfig loaded. endpointName:', endpointName); +} + +export function getEndpointName() { + return endpointName; +} + +export function getEndpoints() { + return Object.keys(urlMap); +} + +export function setEndpointName(newEndpointName) { + if (typeof urlMap[newEndpointName] !== 'string') { + throw new Error(`Unknown endpoint: ${newEndpointName}`); + } + endpointName = newEndpointName; + console.log('endpointName is now', endpointName); + localforage.setItem('endpointName', endpointName).catch(err => { + console.log(`Failed to set endpointName in localforage: ${err}`); + }); +} + +export function getRpcUrl() { + return urlMap[endpointName]; +} + +export function getApiUrl() { + const urlParts = urlParse(getRpcUrl()); + urlParts.host = ''; + if (urlParts.protocol === 'https:') { + urlParts.port = '3443'; + } else { + urlParts.port = '3001'; + } + const url = urlFormat(urlParts); + console.info('getApiUrl:', url); + return url; +} + +export function getApiWebsocketUrl() { + const urlParts = urlParse(getApiUrl()); + urlParts.host = ''; + if (urlParts.protocol === 'https:') { + urlParts.protocol = 'wss:'; + urlParts.port = '3444'; + } else { + urlParts.protocol = 'ws:'; + } + const url = urlFormat(urlParts); + console.info('getApiWebsocketUrl:', url); + return url; +} + export function getMetricsDashboardUrl() { - const matches = window.location.hostname.match('(.*).testnet.solana.com'); + let matches; + if (endpointName === 'local') { + matches = window.location.hostname.match('([^.]*).testnet.solana.com'); + } else { + const endpointUrl = urlMap[endpointName]; + matches = endpointUrl.match('([^.]*).testnet.solana.com'); + } + let url = 'https://metrics.solana.com:3000/d/testnet-beta/testnet-monitor-beta?refresh=5s&from=now-5m&to=now'; if (matches) { const testnet = matches[1]; url += `&var-testnet=testnet-${testnet}`; } + console.log('getMetricsDashboardUrl:', url); return url; } - -let EndpointConfig = { - BLOCK_EXPLORER_API_BASE: `//${window.location.hostname}:3001`, - BLOCK_EXPLORER_RPC_URL: `http://${window.location.hostname}:8899`, -}; - -export default EndpointConfig; diff --git a/src/index.js b/src/index.js index 7fa9335e..c7fa0ad7 100755 --- a/src/index.js +++ b/src/index.js @@ -6,13 +6,18 @@ import './index.css'; import App from './App'; import AppV2 from './AppV2'; import * as serviceWorker from './serviceWorker'; +import * as EndpointConfig from './EndpointConfig'; -ReactDOM.render( - - {window.location.pathname.includes('rc') ? : } - , - document.getElementById('root'), -); +async function main() { + await EndpointConfig.load(); + ReactDOM.render( + + {window.location.pathname.includes('rc') ? : } + , + document.getElementById('root'), + ); +} +main(); // If you want your app to work offline and load faster, you can change // unregister() to register() below. Note this comes with some pitfalls. diff --git a/src/v1/BxAppBar.jsx b/src/v1/BxAppBar.jsx index b48b7505..b2347f01 100644 --- a/src/v1/BxAppBar.jsx +++ b/src/v1/BxAppBar.jsx @@ -15,7 +15,8 @@ import MapIcon from '@material-ui/icons/Map'; // import AccountCircle from '@material-ui/icons/AccountCircle'; import MoreIcon from '@material-ui/icons/MoreVert'; import Switch from '@material-ui/core/Switch'; -import {getMetricsDashboardUrl} from '../EndpointConfig'; +import Select from '@material-ui/core/Select'; +import * as EndpointConfig from '../EndpointConfig'; class BxAppBar extends React.Component { state = { @@ -24,7 +25,7 @@ class BxAppBar extends React.Component { }; handleDashboard = event => { - window.open(getMetricsDashboardUrl()); + window.open(EndpointConfig.getMetricsDashboardUrl()); }; handleMap = event => { @@ -62,6 +63,12 @@ class BxAppBar extends React.Component { this.setState({mobileMoreAnchorEl: null}); }; + handleSetEndpointName = event => { + if (this.props.handleSetEndpointName) { + this.props.handleSetEndpointName(event); + } + }; + render() { const {anchorEl, mobileMoreAnchorEl} = this.state; const {classes} = this.props; @@ -180,6 +187,19 @@ class BxAppBar extends React.Component { {/*>*/} {/**/} {/**/} +