Skip to content
This repository has been archived by the owner on Jul 22, 2020. It is now read-only.

Commit

Permalink
feat: Add global node map 0.1
Browse files Browse the repository at this point in the history
  • Loading branch information
mvines committed Apr 25, 2019
1 parent 0f52617 commit c490e79
Show file tree
Hide file tree
Showing 7 changed files with 371 additions and 21 deletions.
11 changes: 11 additions & 0 deletions api/api.js
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import WebSocket from 'ws';
import _ from 'lodash';
import './inbound-stream';
import expressWs from 'express-ws';
import geoip from 'geoip-lite';

import config from './config';

Expand Down Expand Up @@ -252,6 +253,16 @@ app.get('/blk/:id', (req, res) => {
sendBlockResult(req, res);
});

app.get('/geoip/:ip', (req, res) => {
const {ip} = req.params;
const geo = geoip.lookup(ip);
if (geo === null) {
res.status(404).send('{"error":"not_found"}\n');
} else {
res.send(JSON.stringify(geo.ll) + '\n');
}
});

async function sendEntryResult(req, res) {
try {
let result = await hgetallAsync(`!ent:${req.params.id}`);
Expand Down
2 changes: 2 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,8 @@
"cors": "^2.8.5",
"express": "^4.16.4",
"express-ws": "^4.0.0",
"geoip-lite": "^1.3.7",
"google-map-react": "^1.1.4",
"ip": "^1.1.5",
"lodash": "^4.17.11",
"nocache": "^2.0.0",
Expand Down
77 changes: 71 additions & 6 deletions src/App.js
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ import BxTransactionChart from './BxTransactionChart';
import BxStatsTable from './BxStatsTable';
import BxDialog from './BxDialog';
import BxDialogTransactions from './BxDialogTransactions';
import BxDialogWorldMap from './BxDialogWorldMap';
import BxAppBar from './BxAppBar';

const history = createBrowserHistory();
Expand Down Expand Up @@ -103,11 +104,35 @@ const styles = theme => ({
},
});

async function geoip(ip: string) {
let lat = 11.6065;
let lng = 165.3768;

try {
const result = await window.fetch(
`http:${BLOCK_EXPLORER_API_BASE}/geoip/${ip}`,
);
if (result.status === 200) {
const json = await result.json();
lat = json[0];
lng = json[1];
console.log(ip, 'at', lat, lng);
}
} catch (err) {
console.log('geoip of', ip, 'failed with:', err);
}

// Add some Math.random() to prevent nodes in the same data center from fully
// overlaying each other
return [lat + Math.random() / 10 - 0.05, lng + Math.random() / 10 - 0.05];
}

const BLOCK_EXPLORER_API_BASE = EndpointConfig.BLOCK_EXPLORER_API_BASE;

const BxAppBarThemed = withStyles(styles)(BxAppBar);
const BxDialogThemed = withStyles(styles)(BxDialog);
const BxDialogTransactionsThemed = withStyles(styles)(BxDialogTransactions);
const BxDialogWorldMapThemed = withStyles(styles)(BxDialogWorldMap);
const BxStatsTableThemed = withStyles(styles)(BxStatsTable);
const BxTransactionChartThemed = withStyles(styles)(BxTransactionChart);
const BxDataTableThemed = withStyles(styles)(BxDataTable);
Expand Down Expand Up @@ -138,8 +163,9 @@ class App extends Component {
selectedValue: null,
currentMatch: null,
stateLoading: false,
nodes: [],
globalStats: {
'node-count': 0,
'!ent-last-leader': null,
'!blk-last-slot': 0,
'!txn-count': 0,
'!txn-per-sec-max': 0,
Expand Down Expand Up @@ -215,11 +241,31 @@ class App extends Component {
);

try {
const nodes = await this.connection.getClusterNodes();
this.updateSpecificGlobalStateAttribute('node-count', nodes.length);
const oldNodes = this.state.nodes;
const newNodes = await this.connection.getClusterNodes();

let modified = oldNodes.length !== newNodes.length;
for (const newNode of newNodes) {
const oldNode = oldNodes.find(node => node.id === newNode.id);
if (oldNode) {
newNode.lat = oldNode.lat;
newNode.lng = oldNode.lng;
} else {
const ip = newNode.gossip.split(':')[0];
const [lat, lng] = await geoip(ip);
newNode.lat = lat;
newNode.lng = lng;
modified = true;
}
}

if (modified) {
console.log('newNodes', newNodes);
this.setState({nodes: newNodes});
}
} catch (err) {
this.updateSpecificGlobalStateAttribute('node-count', '?');
console.log(err);
this.setState({nodes: []});
console.log('getClusterNodes failed:', err.message);
}
}

Expand Down Expand Up @@ -471,6 +517,10 @@ class App extends Component {
history.push('/');
};

showMap = () => () => {
history.push(`/map`);
};

toggleEnabled = self => event => {
if (event.target.checked === self.state.enabled) {
return;
Expand Down Expand Up @@ -581,8 +631,20 @@ class App extends Component {
handleSearch={self.handleSearch(self)}
enabled={this.state.enabled}
handleSwitch={this.toggleEnabled(self)}
handleMap={this.showMap(self)}
/>
<div>
<Route
path="/map"
render={() => (
<BxDialogWorldMapThemed
open={true}
onClose={self.handleDialogClose}
nodes={this.state.nodes}
leaderId={this.state.globalStats['!ent-last-leader']}
/>
)}
/>
<Route
path="/txn/:id"
exact
Expand Down Expand Up @@ -629,7 +691,10 @@ class App extends Component {
/>
</div>
<p />
<BxStatsTableThemed globalStats={this.state.globalStats} />
<BxStatsTableThemed
globalStats={this.state.globalStats}
nodeCount={this.state.nodes.length}
/>
<p />
<BxTransactionChartThemed txnStats={this.state.txnStats} />
<p />
Expand Down
20 changes: 10 additions & 10 deletions src/BxAppBar.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import MenuItem from '@material-ui/core/MenuItem';
import Menu from '@material-ui/core/Menu';
//import MenuIcon from '@material-ui/icons/Menu';
import SearchIcon from '@material-ui/icons/Search';
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';
Expand All @@ -20,6 +21,12 @@ class BxAppBar extends React.Component {
mobileMoreAnchorEl: null,
};

handleMap = event => {
if (this.props.handleMap) {
this.props.handleMap(event);
}
};

handleSearch = event => {
if (this.props.handleSearch) {
this.props.handleSearch(event);
Expand Down Expand Up @@ -144,16 +151,9 @@ class BxAppBar extends React.Component {
</div>
<div className={classes.grow} />
<div className={classes.sectionDesktop}>
{/*<IconButton color="inherit">*/}
{/*<Badge badgeContent={4} color="secondary">*/}
{/*<MailIcon />*/}
{/*</Badge>*/}
{/*</IconButton>*/}
{/*<IconButton color="inherit">*/}
{/*<Badge badgeContent={17} color="secondary">*/}
{/*<NotificationsIcon />*/}
{/*</Badge>*/}
{/*</IconButton>*/}
<IconButton color="inherit" onClick={this.handleMap}>
<MapIcon />
</IconButton>
<Switch
checked={this.props.enabled}
onChange={this.handleSwitch}
Expand Down
162 changes: 162 additions & 0 deletions src/BxDialogWorldMap.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,162 @@
import React from 'react';
import CloseIcon from '@material-ui/icons/Close';
import ComputerIcon from '@material-ui/icons/Computer';
import Dialog from '@material-ui/core/Dialog';
import DialogTitle from '@material-ui/core/DialogTitle';
import IconButton from '@material-ui/core/IconButton';
import PropTypes from 'prop-types';
import GoogleMapReact from 'google-map-react';
import Fab from '@material-ui/core/Fab';
import Popover from '@material-ui/core/Popover';
import {Connection} from '@solana/web3.js';
import Typography from '@material-ui/core/Typography';
import Button from '@material-ui/core/Button';

const mapApiKey = process.env.REACT_APP_MAP_API_KEY || 'AIzaSyArM4e0n53tWyK5drjXP03OmovvVJHk8OU';

class Node extends React.Component {
state = {
anchorEl: null,
};

handleClick = event => {
this.setState({
anchorEl: event.currentTarget,
});
};

handleClose = () => {
this.setState({
anchorEl: null,
});
};

handleTerminate = async () => {
const {node} = this.props;
try {
const rpcUrl = `http://${node.rpc}`;
console.log(rpcUrl);
if (window.confirm('Are you sure you want to terminate this node?')) {
const connection = new Connection(rpcUrl);
const result = await connection.fullnodeExit();
if (!result) {
window.alert('Node declined to exit');
}
}
} catch (err) {
window.alert(`Failed to terminate node: ${err}`);
}
this.handleClose();
};

render() {
const {classes, isLeader, node, ...other} = this.props;
const {anchorEl} = this.state;
const open = Boolean(anchorEl);

return (
<div>
<Fab
size="small"
color={isLeader ? 'secondary' : 'primary'}
aria-owns={open ? 'simple-popper' : undefined}
aria-haspopup="true"
variant="contained"
onClick={this.handleClick}
{...other}
>
<ComputerIcon />
</Fab>
<Popover
id="simple-popper"
open={open}
anchorEl={anchorEl}
onClose={this.handleClose}
anchorOrigin={{
vertical: 'bottom',
horizontal: 'center',
}}
transformOrigin={{
vertical: 'top',
horizontal: 'center',
}}
>
<Typography>
<b>Node:</b> {node.id}
<br />
<b>Gossip:</b> {node.gossip}
</Typography>
{node.rpc && (
<div style={{textAlign: 'center'}}>
<p />
<Button onClick={this.handleTerminate}>Terminate Node</Button>
</div>
)}
</Popover>
</div>
);
}
}

Node.propTypes = {
classes: PropTypes.object.isRequired,
};

export default class BxDialogWorldMap extends React.Component {
static defaultProps = {
center: {
lat: 38.747,
lng: -120.6743,
},
zoom: 1,
};

render() {
const {classes, onClose, leaderId, nodes, ...other} = this.props;

return (
<Dialog
onClose={onClose}
aria-labelledby="simple-dialog-title"
fullScreen={true}
{...other}
>
<DialogTitle id="simple-dialog-title">
Cluster Map: <i>{nodes.length} nodes</i>
<IconButton
aria-label="Close"
className={classes.closeButton}
onClick={onClose}
>
<CloseIcon />
</IconButton>
</DialogTitle>
<div style={{height: '90vh', width: '100%'}}>
<GoogleMapReact
bootstrapURLKeys={{key: mapApiKey}}
defaultCenter={this.props.center}
defaultZoom={this.props.zoom}
>
{nodes.map(node => {
return (
<Node
key={node.id}
node={node}
isLeader={node.id === leaderId}
lat={node.lat}
lng={node.lng}
classes={classes}
/>
);
})}
</GoogleMapReact>
</div>
</Dialog>
);
}
}

BxDialogWorldMap.propTypes = {
classes: PropTypes.object.isRequired,
onClose: PropTypes.func,
};
4 changes: 2 additions & 2 deletions src/BxStatsTable.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ class BxStatsTable extends React.Component {
}

render() {
const {globalStats} = this.props;
const {globalStats, nodeCount} = this.props;

let currentTpsKey = null;
_.forEach(globalStats, (v, k) => {
Expand Down Expand Up @@ -51,7 +51,7 @@ class BxStatsTable extends React.Component {
<BxHelpLink text="Node Count" term="node-count" />
</Typography>
<Typography component="p" align="center">
{globalStats['node-count']}
{nodeCount}
</Typography>
</CardContent>
</Card>
Expand Down
Loading

0 comments on commit c490e79

Please sign in to comment.