diff --git a/api/api.js b/api/api.js index 6427e123..aa591375 100644 --- a/api/api.js +++ b/api/api.js @@ -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'; @@ -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}`); diff --git a/package.json b/package.json index 379f632d..92d1d77a 100644 --- a/package.json +++ b/package.json @@ -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", diff --git a/src/App.js b/src/App.js index fcdc8536..b4651260 100755 --- a/src/App.js +++ b/src/App.js @@ -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(); @@ -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); @@ -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, @@ -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); } } @@ -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; @@ -581,8 +631,20 @@ class App extends Component { handleSearch={self.handleSearch(self)} enabled={this.state.enabled} handleSwitch={this.toggleEnabled(self)} + handleMap={this.showMap(self)} />
+ ( + + )} + />

- +

diff --git a/src/BxAppBar.jsx b/src/BxAppBar.jsx index c7a218a5..15f60f34 100644 --- a/src/BxAppBar.jsx +++ b/src/BxAppBar.jsx @@ -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'; @@ -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); @@ -144,16 +151,9 @@ class BxAppBar extends React.Component {

- {/**/} - {/**/} - {/**/} - {/**/} - {/**/} - {/**/} - {/**/} - {/**/} - {/**/} - {/**/} + + + { + 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 ( +
+ + + + + + Node: {node.id} +
+ Gossip: {node.gossip} +
+ {node.rpc && ( +
+

+ +

+ )} +
+
+ ); + } +} + +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 ( + + + Cluster Map: {nodes.length} nodes + + + + +
+ + {nodes.map(node => { + return ( + + ); + })} + +
+
+ ); + } +} + +BxDialogWorldMap.propTypes = { + classes: PropTypes.object.isRequired, + onClose: PropTypes.func, +}; diff --git a/src/BxStatsTable.jsx b/src/BxStatsTable.jsx index 6255542d..d0dd63bb 100644 --- a/src/BxStatsTable.jsx +++ b/src/BxStatsTable.jsx @@ -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) => { @@ -51,7 +51,7 @@ class BxStatsTable extends React.Component { - {globalStats['node-count']} + {nodeCount} diff --git a/yarn.lock b/yarn.lock index 4e25cacc..4a5b4fee 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1247,6 +1247,11 @@ "@types/istanbul-lib-coverage" "^2.0.0" "@types/yargs" "^12.0.9" +"@mapbox/point-geometry@^0.1.0": + version "0.1.0" + resolved "https://registry.yarnpkg.com/@mapbox/point-geometry/-/point-geometry-0.1.0.tgz#8a83f9335c7860effa2eeeca254332aa0aeed8f2" + integrity sha1-ioP5M1x4YO/6Lu7KJUMyqgru2PI= + "@material-ui/core@^3.9.3": version "3.9.3" resolved "https://registry.yarnpkg.com/@material-ui/core/-/core-3.9.3.tgz#d378c1f4beb18df9a534ca7258c2c33fb8e0e51f" @@ -2307,7 +2312,7 @@ async@^1.5.2: resolved "https://registry.yarnpkg.com/async/-/async-1.5.2.tgz#ec6a61ae56480c0c3cb241c95618e20892f9672a" integrity sha1-7GphrlZIDAw8skHJVhjiCJL5Zyo= -async@^2.5.0, async@^2.6.1: +async@^2.1.1, async@^2.5.0, async@^2.6.1: version "2.6.2" resolved "https://registry.yarnpkg.com/async/-/async-2.6.2.tgz#18330ea7e6e313887f5d2f2a904bac6fe4dd5381" integrity sha512-H1qVYh1MYhEEFLsP97cVKqCGo7KfCyTt6uEWqsTBr9SO84oK9Uwbyd/yCW+6rKJLHksBNUVWZDAjfS+Ccx0Bbg== @@ -3353,6 +3358,11 @@ btoa-lite@^1.0.0: resolved "https://registry.yarnpkg.com/btoa-lite/-/btoa-lite-1.0.0.tgz#337766da15801210fdd956c22e9c6891ab9d0337" integrity sha1-M3dm2hWAEhD92VbCLpxokaudAzc= +buffer-crc32@~0.2.3: + version "0.2.13" + resolved "https://registry.yarnpkg.com/buffer-crc32/-/buffer-crc32-0.2.13.tgz#0d333e3f00eac50aa1454abd30ef8c2a5d9a7242" + integrity sha1-DTM+PwDqxQqhRUq9MO+MKl2ackI= + buffer-from@^1.0.0: version "1.1.1" resolved "https://registry.yarnpkg.com/buffer-from/-/buffer-from-1.1.1.tgz#32713bc028f75c02fdb710d7c7bcec1f2c6070ef" @@ -5493,6 +5503,11 @@ etag@~1.8.1: resolved "https://registry.yarnpkg.com/etag/-/etag-1.8.1.tgz#41ae2eeb65efa62268aebfea83ac7d79299b0887" integrity sha1-Qa4u62XvpiJorr/qg6x9eSmbCIc= +eventemitter3@^1.1.0: + version "1.2.0" + resolved "https://registry.yarnpkg.com/eventemitter3/-/eventemitter3-1.2.0.tgz#1c86991d816ad1e504750e73874224ecf3bec508" + integrity sha1-HIaZHYFq0eUEdQ5zh0Ik7PO+xQg= + eventemitter3@^3.0.0, eventemitter3@^3.1.0: version "3.1.0" resolved "https://registry.yarnpkg.com/eventemitter3/-/eventemitter3-3.1.0.tgz#090b4d6cdbd645ed10bf750d4b5407942d7ba163" @@ -5802,6 +5817,13 @@ fbjs@^0.8.0, fbjs@^0.8.1: setimmediate "^1.0.5" ua-parser-js "^0.7.18" +fd-slicer@~1.1.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/fd-slicer/-/fd-slicer-1.1.0.tgz#25c7c89cb1f9077f8891bbe61d8f390eae256f1e" + integrity sha1-JcfInLH5B3+IkbvmHY85Dq4lbx4= + dependencies: + pend "~1.2.0" + figgy-pudding@^3.0.0, figgy-pudding@^3.1.0, figgy-pudding@^3.4.1, figgy-pudding@^3.5.1: version "3.5.1" resolved "https://registry.yarnpkg.com/figgy-pudding/-/figgy-pudding-3.5.1.tgz#862470112901c727a0e495a80744bd5baa1d6790" @@ -6181,6 +6203,19 @@ gentle-fs@^2.0.0, gentle-fs@^2.0.1: read-cmd-shim "^1.0.1" slide "^1.1.6" +geoip-lite@^1.3.7: + version "1.3.7" + resolved "https://registry.yarnpkg.com/geoip-lite/-/geoip-lite-1.3.7.tgz#6fce24bf4a23b06bd2dce6b2d20c8e7f49b8437a" + integrity sha512-hdlRVXTk/cFFXbeDl0KdcDSH2Gzlyp7050Q+ml+g9Y0BkygjDklwF/nKcouzHWWzPsBRIp0EJXiKaTqJ0hlDSQ== + dependencies: + async "^2.1.1" + colors "^1.1.2" + iconv-lite "^0.4.13" + ip-address "^5.8.9" + lazy "^1.0.11" + rimraf "^2.5.2" + yauzl "^2.9.2" + get-caller-file@^1.0.1: version "1.0.3" resolved "https://registry.yarnpkg.com/get-caller-file/-/get-caller-file-1.0.3.tgz#f978fa4c90d1dfe7ff2d6beda2a515e713bdcf4a" @@ -6342,6 +6377,15 @@ globby@^9.0.0: pify "^4.0.1" slash "^2.0.0" +google-map-react@^1.1.4: + version "1.1.4" + resolved "https://registry.yarnpkg.com/google-map-react/-/google-map-react-1.1.4.tgz#ebf649b3cb985c645a0b7a05eb4e1c3f80482c80" + integrity sha512-+XcOu7QrRrlDa7Bk25oVV4FSsvMorAupuBrnDjA28Zz+HE+wEz79igqn+0KCHbemjgEHz51sHk0wm+IodvLoMg== + dependencies: + "@mapbox/point-geometry" "^0.1.0" + eventemitter3 "^1.1.0" + scriptjs "^2.5.7" + got@^6.7.1: version "6.7.1" resolved "https://registry.yarnpkg.com/got/-/got-6.7.1.tgz#240cd05785a9a18e561dc1b44b41c763ef1e8db0" @@ -6766,7 +6810,7 @@ iconv-lite@0.4.23: dependencies: safer-buffer ">= 2.1.2 < 3" -iconv-lite@0.4.24, iconv-lite@^0.4.24, iconv-lite@^0.4.4, iconv-lite@~0.4.13: +iconv-lite@0.4.24, iconv-lite@^0.4.13, iconv-lite@^0.4.24, iconv-lite@^0.4.4, iconv-lite@~0.4.13: version "0.4.24" resolved "https://registry.yarnpkg.com/iconv-lite/-/iconv-lite-0.4.24.tgz#2022b4b25fbddc21d2f524974a474aafe733908b" integrity sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA== @@ -6988,6 +7032,19 @@ invert-kv@^2.0.0: resolved "https://registry.yarnpkg.com/invert-kv/-/invert-kv-2.0.0.tgz#7393f5afa59ec9ff5f67a27620d11c226e3eec02" integrity sha512-wPVv/y/QQ/Uiirj/vh3oP+1Ww+AWehmi1g5fFWGPF6IpCBCDVrhgHRMvrLfdYcwDh3QJbGXDW4JAuzxElLSqKA== +ip-address@^5.8.9: + version "5.9.0" + resolved "https://registry.yarnpkg.com/ip-address/-/ip-address-5.9.0.tgz#e501b3a0da9e31d9a14ef7ccdc05c5552c465954" + integrity sha512-+4yKpEyent8IpjuDQVkIpzIDbxSlCHTPdmaXCRLH0ttt3YsrbNxuZJ6h+1wLPx10T7gWsLN7M6BXIHV2vZNOGw== + dependencies: + jsbn "1.1.0" + lodash.find "^4.6.0" + lodash.max "^4.0.1" + lodash.merge "^4.6.1" + lodash.padstart "^4.6.1" + lodash.repeat "^4.1.0" + sprintf-js "1.1.1" + ip-regex@^2.1.0: version "2.1.0" resolved "https://registry.yarnpkg.com/ip-regex/-/ip-regex-2.1.0.tgz#fa78bf5d2e6913c911ce9f819ee5146bb6d844e9" @@ -7949,6 +8006,11 @@ js-yaml@^3.12.0, js-yaml@^3.13.0, js-yaml@^3.9.0: argparse "^1.0.7" esprima "^4.0.0" +jsbn@1.1.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/jsbn/-/jsbn-1.1.0.tgz#b01307cb29b618a1ed26ec79e911f803c4da0040" + integrity sha1-sBMHyym2GKHtJux56RH4A8TaAEA= + jsbn@~0.1.0: version "0.1.1" resolved "https://registry.yarnpkg.com/jsbn/-/jsbn-0.1.1.tgz#a5e654c2e5a2deb5f201d96cefbca80c0ef2f513" @@ -8260,6 +8322,11 @@ lazy-property@~1.0.0: resolved "https://registry.yarnpkg.com/lazy-property/-/lazy-property-1.0.0.tgz#84ddc4b370679ba8bd4cdcfa4c06b43d57111147" integrity sha1-hN3Es3Bnm6i9TNz6TAa0PVcREUc= +lazy@^1.0.11: + version "1.0.11" + resolved "https://registry.yarnpkg.com/lazy/-/lazy-1.0.11.tgz#daa068206282542c088288e975c297c1ae77b690" + integrity sha1-2qBoIGKCVCwIgojpdcKXwa53tpA= + lcid@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/lcid/-/lcid-1.0.0.tgz#308accafa0bc483a3867b4b6f2b9506251d1b835" @@ -8470,6 +8537,11 @@ lodash.filter@^4.4.0: resolved "https://registry.yarnpkg.com/lodash.filter/-/lodash.filter-4.6.0.tgz#668b1d4981603ae1cc5a6fa760143e480b4c4ace" integrity sha1-ZosdSYFgOuHMWm+nYBQ+SAtMSs4= +lodash.find@^4.6.0: + version "4.6.0" + resolved "https://registry.yarnpkg.com/lodash.find/-/lodash.find-4.6.0.tgz#cb0704d47ab71789ffa0de8b97dd926fb88b13b1" + integrity sha1-ywcE1Hq3F4n/oN6Ll92Sb7iLE7E= + lodash.flatten@^4.2.0: version "4.4.0" resolved "https://registry.yarnpkg.com/lodash.flatten/-/lodash.flatten-4.4.0.tgz#f31c22225a9632d2bbf8e4addbef240aa765a61f" @@ -8500,16 +8572,26 @@ lodash.map@^4.4.0: resolved "https://registry.yarnpkg.com/lodash.map/-/lodash.map-4.6.0.tgz#771ec7839e3473d9c4cde28b19394c3562f4f6d3" integrity sha1-dx7Hg540c9nEzeKLGTlMNWL09tM= +lodash.max@^4.0.1: + version "4.0.1" + resolved "https://registry.yarnpkg.com/lodash.max/-/lodash.max-4.0.1.tgz#8735566c618b35a9f760520b487ae79658af136a" + integrity sha1-hzVWbGGLNan3YFILSHrnllivE2o= + lodash.memoize@^4.1.2: version "4.1.2" resolved "https://registry.yarnpkg.com/lodash.memoize/-/lodash.memoize-4.1.2.tgz#bcc6c49a42a2840ed997f323eada5ecd182e0bfe" integrity sha1-vMbEmkKihA7Zl/Mj6tpezRguC/4= -lodash.merge@^4.4.0: +lodash.merge@^4.4.0, lodash.merge@^4.6.1: version "4.6.1" resolved "https://registry.yarnpkg.com/lodash.merge/-/lodash.merge-4.6.1.tgz#adc25d9cb99b9391c59624f379fbba60d7111d54" integrity sha512-AOYza4+Hf5z1/0Hztxpm2/xiPZgi/cjMqdnKTUWTBSKchJlxXXuUSxCCl8rJlf4g6yww/j6mA8nC8Hw/EZWxKQ== +lodash.padstart@^4.6.1: + version "4.6.1" + resolved "https://registry.yarnpkg.com/lodash.padstart/-/lodash.padstart-4.6.1.tgz#d2e3eebff0d9d39ad50f5cbd1b52a7bce6bb611b" + integrity sha1-0uPuv/DZ05rVD1y9G1KnvOa7YRs= + lodash.pick@^4.2.1: version "4.4.0" resolved "https://registry.yarnpkg.com/lodash.pick/-/lodash.pick-4.4.0.tgz#52f05610fff9ded422611441ed1fc123a03001b3" @@ -8525,6 +8607,11 @@ lodash.reject@^4.4.0: resolved "https://registry.yarnpkg.com/lodash.reject/-/lodash.reject-4.6.0.tgz#80d6492dc1470864bbf583533b651f42a9f52415" integrity sha1-gNZJLcFHCGS79YNTO2UfQqn1JBU= +lodash.repeat@^4.1.0: + version "4.1.0" + resolved "https://registry.yarnpkg.com/lodash.repeat/-/lodash.repeat-4.1.0.tgz#fc7de8131d8c8ac07e4b49f74ffe829d1f2bec44" + integrity sha1-/H3oEx2MisB+S0n3T/6CnR8r7EQ= + lodash.set@^4.3.2: version "4.3.2" resolved "https://registry.yarnpkg.com/lodash.set/-/lodash.set-4.3.2.tgz#d8757b1da807dde24816b0d6a84bea1a76230b23" @@ -10206,6 +10293,11 @@ pbkdf2@^3.0.3: safe-buffer "^5.0.1" sha.js "^2.4.8" +pend@~1.2.0: + version "1.2.0" + resolved "https://registry.yarnpkg.com/pend/-/pend-1.2.0.tgz#7a57eb550a6783f9115331fcf4663d5c8e007a50" + integrity sha1-elfrVQpng/kRUzH89GY9XI4AelA= + performance-now@^2.1.0: version "2.1.0" resolved "https://registry.yarnpkg.com/performance-now/-/performance-now-2.1.0.tgz#6309f4e0e5fa913ec1c69307ae364b4b377c9e7b" @@ -12200,6 +12292,11 @@ schema-utils@^1.0.0: ajv-errors "^1.0.0" ajv-keywords "^3.1.0" +scriptjs@^2.5.7: + version "2.5.9" + resolved "https://registry.yarnpkg.com/scriptjs/-/scriptjs-2.5.9.tgz#343915cd2ec2ed9bfdde2b9875cd28f59394b35f" + integrity sha512-qGVDoreyYiP1pkQnbnFAUIS5AjenNwwQBdl7zeos9etl+hYKWahjRTfzAZZYBv5xNHx7vNKCmaLDQZ6Fr2AEXg== + select-hose@^2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/select-hose/-/select-hose-2.0.0.tgz#625d8658f865af43ec962bfc376a37359a4994ca" @@ -12739,6 +12836,11 @@ split@^1.0.0: dependencies: through "2" +sprintf-js@1.1.1: + version "1.1.1" + resolved "https://registry.yarnpkg.com/sprintf-js/-/sprintf-js-1.1.1.tgz#36be78320afe5801f6cea3ee78b6e5aab940ea0c" + integrity sha1-Nr54Mgr+WAH2zqPueLblqrlA6gw= + sprintf-js@~1.0.2: version "1.0.3" resolved "https://registry.yarnpkg.com/sprintf-js/-/sprintf-js-1.0.3.tgz#04e6926f662895354f3dd015203633b857297e2c" @@ -14351,3 +14453,11 @@ yargs@^12.0.0, yargs@^12.0.2, yargs@^12.0.5: which-module "^2.0.0" y18n "^3.2.1 || ^4.0.0" yargs-parser "^11.1.1" + +yauzl@^2.9.2: + version "2.10.0" + resolved "https://registry.yarnpkg.com/yauzl/-/yauzl-2.10.0.tgz#c7eb17c93e112cb1086fa6d8e51fb0667b79a5f9" + integrity sha1-x+sXyT4RLLEIb6bY5R+wZnt5pfk= + dependencies: + buffer-crc32 "~0.2.3" + fd-slicer "~1.1.0"