diff --git a/packages/backend/src/Chain.ts b/packages/backend/src/Chain.ts index 679db95..9863da8 100644 --- a/packages/backend/src/Chain.ts +++ b/packages/backend/src/Chain.ts @@ -44,6 +44,7 @@ export default class Chain { node.events.on('block', () => this.updateBlock(node)); node.events.on('stats', () => this.feeds.broadcast(Feed.stats(node))); + node.events.on('location', (location) => this.feeds.broadcast(Feed.locatedNode(node, location))); } public addFeed(feed: Feed) { diff --git a/packages/backend/src/Feed.ts b/packages/backend/src/Feed.ts index f5bbe86..a9bb75c 100644 --- a/packages/backend/src/Feed.ts +++ b/packages/backend/src/Feed.ts @@ -3,6 +3,7 @@ import * as EventEmitter from 'events'; import Node from './Node'; import Chain from './Chain'; import { VERSION, timestamp, Maybe, FeedMessage, Types, idGenerator } from '@dotstats/common'; +import { Location } from './location'; const nextId = idGenerator(); const { Actions } = FeedMessage; @@ -42,7 +43,7 @@ export default class Feed { public static addedNode(node: Node): FeedMessage.Message { return { action: Actions.AddedNode, - payload: [node.id, node.nodeDetails(), node.nodeStats(), node.blockDetails()] + payload: [node.id, node.nodeDetails(), node.nodeStats(), node.blockDetails(), node.nodeLocation()] }; } @@ -53,6 +54,13 @@ export default class Feed { }; } + public static locatedNode(node: Node, location: Location): FeedMessage.Message { + return { + action: Actions.LocatedNode, + payload: [node.id, location.lat, location.lon] + }; + } + public static imported(node: Node): FeedMessage.Message { return { action: Actions.ImportedBlock, diff --git a/packages/backend/src/Node.ts b/packages/backend/src/Node.ts index 1a92b2b..40d1278 100644 --- a/packages/backend/src/Node.ts +++ b/packages/backend/src/Node.ts @@ -1,14 +1,19 @@ import * as WebSocket from 'ws'; import * as EventEmitter from 'events'; -import * as iplocation from 'iplocation'; import { timestamp, Maybe, Types, idGenerator } from '@dotstats/common'; import { parseMessage, getBestBlock, Message, BestBlock, SystemInterval } from './message'; +import { locate, Location } from './location'; const BLOCK_TIME_HISTORY = 10; const TIMEOUT = (1000 * 60 * 1) as Types.Milliseconds; // 1 minute const nextId = idGenerator(); +export interface NodeEvents { + on(event: 'location', fn: (location: Location) => void): void; + emit(event: 'location', location: Location): void; +} + export default class Node { public readonly id: Types.NodeId; public readonly name: Types.NodeName; @@ -16,8 +21,9 @@ export default class Node { public readonly implementation: Types.NodeImplementation; public readonly version: Types.NodeVersion; - public readonly events = new EventEmitter(); + public readonly events = new EventEmitter() as EventEmitter & NodeEvents; + public location: Maybe = null; public lastMessage: Types.Timestamp; public config: string; public best = '' as Types.BlockHash; @@ -30,11 +36,13 @@ export default class Node { private peers = 0 as Types.PeerCount; private txcount = 0 as Types.TransactionCount; + private readonly ip: string; private readonly socket: WebSocket; private blockTimes: Array = new Array(BLOCK_TIME_HISTORY); private lastBlockAt: Maybe = null; constructor( + ip: string, socket: WebSocket, name: Types.NodeName, chain: Types.ChainLabel, @@ -42,6 +50,7 @@ export default class Node { implentation: Types.NodeImplementation, version: Types.NodeVersion, ) { + this.ip = ip; this.id = nextId(); this.name = name; this.chain = chain; @@ -83,6 +92,18 @@ export default class Node { this.disconnect(); }); + + locate(ip).then((location) => { + if (!location) { + return; + } + + console.log('node', ip, 'located at', location); + + this.location = location; + + this.events.emit('location', location); + }); } public static fromSocket(socket: WebSocket, ip: string): Promise { @@ -102,7 +123,7 @@ export default class Node { const { name, chain, config, implementation, version } = message; - resolve(new Node(socket, name, chain, config, implementation, version)); + resolve(new Node(ip, socket, name, chain, config, implementation, version)); } } @@ -136,6 +157,12 @@ export default class Node { return [this.height, this.best, this.blockTime, this.blockTimestamp, this.propagationTime]; } + public nodeLocation(): Maybe { + const { location } = this; + + return location ? [location.lat, location.lon] : null; + } + public get average(): number { let accounted = 0; let sum = 0; diff --git a/packages/backend/src/location.ts b/packages/backend/src/location.ts new file mode 100644 index 0000000..081ed02 --- /dev/null +++ b/packages/backend/src/location.ts @@ -0,0 +1,41 @@ +import * as iplocation from 'iplocation'; +import { Maybe, Types } from '@dotstats/common'; + +export interface Location { + lat: Types.Latitude; + lon: Types.Longitude; +} + +const cache = new Map(); + +export async function locate(ip: string): Promise> { + if (ip === '127.0.0.1') { + return Promise.resolve({ + lat: 52.5166667 as Types.Latitude, + lon: 13.4 as Types.Longitude + }); + } + + const cached = cache.get(ip); + + if (cached) { + return Promise.resolve(cached); + } + + return new Promise>((resolve, _) => { + iplocation(ip, (err, result) => { + if (err) { + console.error(`Couldn't locate ${ip}`); + + return resolve(null); + } + + const { lat, lon } = result; + const location = { lat, lon } as Location; + + cache.set(ip, location); + + resolve(location); + }); + }) +} diff --git a/packages/common/src/feed.ts b/packages/common/src/feed.ts index 85db6ed..5d1204d 100644 --- a/packages/common/src/feed.ts +++ b/packages/common/src/feed.ts @@ -1,10 +1,13 @@ import { Opaque, Maybe } from './helpers'; import { FeedVersion, + Latitude, + Longitude, NodeId, NodeCount, NodeDetails, NodeStats, + NodeLocation, BlockNumber, BlockDetails, Timestamp, @@ -13,17 +16,18 @@ import { } from './types'; export const Actions = { - FeedVersion: 255 as 255, - BestBlock: 0 as 0, - AddedNode: 1 as 1, - RemovedNode: 2 as 2, - ImportedBlock: 3 as 3, - NodeStats: 4 as 4, - TimeSync: 5 as 5, - AddedChain: 6 as 6, - RemovedChain: 7 as 7, - SubscribedTo: 8 as 8, - UnsubscribedFrom: 9 as 9 + FeedVersion : 0xff as 0xff, + BestBlock : 0x00 as 0x00, + AddedNode : 0x01 as 0x01, + RemovedNode : 0x02 as 0x02, + LocatedNode : 0x03 as 0x03, + ImportedBlock : 0x04 as 0x04, + NodeStats : 0x05 as 0x05, + TimeSync : 0x06 as 0x06, + AddedChain : 0x07 as 0x07, + RemovedChain : 0x08 as 0x08, + SubscribedTo : 0x09 as 0x09, + UnsubscribedFrom : 0x0A as 0x0A, }; export type Action = typeof Actions[keyof typeof Actions]; @@ -46,7 +50,7 @@ export namespace Variants { export interface AddedNodeMessage extends MessageBase { action: typeof Actions.AddedNode; - payload: [NodeId, NodeDetails, NodeStats, BlockDetails]; + payload: [NodeId, NodeDetails, NodeStats, BlockDetails, Maybe]; } export interface RemovedNodeMessage extends MessageBase { @@ -54,6 +58,11 @@ export namespace Variants { payload: NodeId; } + export interface LocatedNodeMessage extends MessageBase { + action: typeof Actions.LocatedNode; + payload: [NodeId, Latitude, Longitude]; + } + export interface ImportedBlockMessage extends MessageBase { action: typeof Actions.ImportedBlock; payload: [NodeId, BlockDetails]; @@ -95,6 +104,7 @@ export type Message = | Variants.BestBlockMessage | Variants.AddedNodeMessage | Variants.RemovedNodeMessage + | Variants.LocatedNodeMessage | Variants.ImportedBlockMessage | Variants.NodeStatsMessage | Variants.TimeSyncMessage diff --git a/packages/common/src/index.ts b/packages/common/src/index.ts index fcfd127..213dfa9 100644 --- a/packages/common/src/index.ts +++ b/packages/common/src/index.ts @@ -1,4 +1,3 @@ -export * from './iterators'; export * from './helpers'; export * from './id'; @@ -7,4 +6,4 @@ import * as FeedMessage from './feed'; export { Types, FeedMessage }; -export const VERSION: Types.FeedVersion = 2 as Types.FeedVersion; +export const VERSION: Types.FeedVersion = 3 as Types.FeedVersion; diff --git a/packages/common/src/types.ts b/packages/common/src/types.ts index e4c9cb8..88aee4d 100644 --- a/packages/common/src/types.ts +++ b/packages/common/src/types.ts @@ -16,7 +16,10 @@ export type PropagationTime = Opaque; export type NodeCount = Opaque; export type PeerCount = Opaque; export type TransactionCount = Opaque; +export type Latitude = Opaque; +export type Longitude = Opaque; export type BlockDetails = [BlockNumber, BlockHash, Milliseconds, Timestamp, Maybe]; export type NodeDetails = [NodeName, NodeImplementation, NodeVersion]; export type NodeStats = [PeerCount, TransactionCount]; +export type NodeLocation = [Latitude, Longitude]; diff --git a/packages/frontend/public/index.html b/packages/frontend/public/index.html index 0031032..2db4285 100644 --- a/packages/frontend/public/index.html +++ b/packages/frontend/public/index.html @@ -8,7 +8,7 @@ Polkadot Telemetry diff --git a/packages/frontend/src/App.tsx b/packages/frontend/src/App.tsx index e2400fb..56cca3c 100644 --- a/packages/frontend/src/App.tsx +++ b/packages/frontend/src/App.tsx @@ -50,7 +50,7 @@ export default class App extends React.Component<{}, State> {
- +
); } diff --git a/packages/frontend/src/Connection.ts b/packages/frontend/src/Connection.ts index 5a65533..09c25c2 100644 --- a/packages/frontend/src/Connection.ts +++ b/packages/frontend/src/Connection.ts @@ -120,8 +120,8 @@ export class Connection { } case Actions.AddedNode: { - const [id, nodeDetails, nodeStats, blockDetails] = message.payload; - const node = { id, nodeDetails, nodeStats, blockDetails }; + const [id, nodeDetails, nodeStats, blockDetails, location] = message.payload; + const node = { id, nodeDetails, nodeStats, blockDetails, location }; nodes.set(id, node); @@ -134,6 +134,19 @@ export class Connection { break; } + case Actions.LocatedNode: { + const [id, latitude, longitude] = message.payload; + const node = nodes.get(id); + + if (!node) { + return; + } + + node.location = [latitude, longitude]; + + break; + } + case Actions.ImportedBlock: { const [id, blockDetails] = message.payload; const node = nodes.get(id); diff --git a/packages/frontend/src/assets/map.svg b/packages/frontend/src/assets/map.svg new file mode 100644 index 0000000..c003b8d --- /dev/null +++ b/packages/frontend/src/assets/map.svg @@ -0,0 +1,75 @@ + + + + + + + + + + + + + image/svg+xml + + + + + + + + + diff --git a/packages/frontend/src/assets/world-map.svg b/packages/frontend/src/assets/world-map.svg new file mode 100644 index 0000000..bd4a89b --- /dev/null +++ b/packages/frontend/src/assets/world-map.svg @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file diff --git a/packages/frontend/src/components/Chain.css b/packages/frontend/src/components/Chain.css index e9a564b..7fd705d 100644 --- a/packages/frontend/src/components/Chain.css +++ b/packages/frontend/src/components/Chain.css @@ -8,12 +8,35 @@ .Chain-content-container { position: absolute; - left: 0; /*80px;*/ + left: 0; right: 0; bottom: 0; top: 148px; } +.Chain-map { + background: url('../assets/world-map.svg') no-repeat; + background-size: contain; + background-position: center; + position: absolute; + left: 0; + right: 0; + top: 0; + bottom: 0; +} + +.Chain-map-node { + width: 10px; + height: 10px; + background: #f00; /* #d64ca8;*/ + border-radius: 5px; + margin-left: -5px; + margin-top: -5px; + position: absolute; + top: 50%; + left: 50%; +} + .Chain-content { width: 100%; min-height: 100%; diff --git a/packages/frontend/src/components/Chain.tsx b/packages/frontend/src/components/Chain.tsx index 20ef9c3..2c9b9a3 100644 --- a/packages/frontend/src/components/Chain.tsx +++ b/packages/frontend/src/components/Chain.tsx @@ -1,5 +1,5 @@ import * as React from 'react'; -import { State } from '../state'; +import { State as AppState } from '../state'; import { formatNumber } from '../utils'; import { Tile, Icon, Node, Ago } from './'; @@ -17,7 +17,11 @@ import './Chain.css'; export namespace Chain { export interface Props { - state: Readonly + appState: Readonly; + } + + export interface State { + display: 'map' | 'table'; } } @@ -34,42 +38,75 @@ function sortNodes(a: Node.Props, b: Node.Props): number { return aPropagation - bPropagation; } -export function Chain(props: Chain.Props) { - const { best, blockTimestamp, blockAverage } = props.state; +export class Chain extends React.Component { + constructor(props: Chain.Props) { + super(props); - const nodes = Array.from(props.state.nodes.values()).sort(sortNodes); + this.state = { + display: 'table' + }; + } - return ( -
-
- #{formatNumber(best)} - { blockAverage == null ? '-' : (blockAverage / 1000).toFixed(3) + 's' } - -
-
-
- - - - - - - - - - - - - - - - { - nodes.map((node) => ) - } - -
+ public render() { + const { best, blockTimestamp, blockAverage } = this.props.appState; + + return ( +
+
+ #{formatNumber(best)} + { blockAverage == null ? '-' : (blockAverage / 1000).toFixed(3) + 's' } + +
+
+
+ { + this.state.display === 'table' + ? this.renderTable() + : this.renderMap() + } +
-
- ); + ); + } + + private renderMap() { + // return ; + return ( +
+ { + this.nodes().map((node) =>
) + } +
+ ); + } + + private renderTable() { + return ( + + + + + + + + + + + + + + + + { + this.nodes().sort(sortNodes).map((node) => ) + } + +
+ ); + } + + private nodes() { + return Array.from(this.props.appState.nodes.values()); + } } diff --git a/packages/frontend/src/components/Chains.css b/packages/frontend/src/components/Chains.css index 02fa93e..9481f6c 100644 --- a/packages/frontend/src/components/Chains.css +++ b/packages/frontend/src/components/Chains.css @@ -29,7 +29,7 @@ padding: 0; margin: 0; position: absolute; - right: 6px; + right: 12px; top: 6px; } @@ -38,10 +38,10 @@ margin: 0; height: 24px; width: 24px; - background: #fff; - border: 2px solid #fff; + background: #3c3c3b; + border: 2px solid #3c3c3b; border-radius: 24px; - color: #555; + color: #ccc; } .Chains-node-count { diff --git a/packages/frontend/src/components/Node.tsx b/packages/frontend/src/components/Node.tsx index 2041733..459f213 100644 --- a/packages/frontend/src/components/Node.tsx +++ b/packages/frontend/src/components/Node.tsx @@ -1,7 +1,7 @@ import * as React from 'react'; import { formatNumber, trimHash } from '../utils'; import { Ago } from './Ago'; -import { Types } from '@dotstats/common'; +import { Types, Maybe } from '@dotstats/common'; export namespace Node { export interface Props { @@ -9,6 +9,7 @@ export namespace Node { nodeDetails: Types.NodeDetails, nodeStats: Types.NodeStats, blockDetails: Types.BlockDetails, + location: Maybe, } }