mirror of
https://github.com/pezkuwichain/pezkuwi-telemetry.git
synced 2026-06-12 14:41:15 +00:00
Working on map view
This commit is contained in:
@@ -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) {
|
||||
|
||||
@@ -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<Types.FeedId>();
|
||||
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,
|
||||
|
||||
@@ -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<Types.NodeId>();
|
||||
|
||||
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<Location> = 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<number> = new Array(BLOCK_TIME_HISTORY);
|
||||
private lastBlockAt: Maybe<Date> = 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<Node> {
|
||||
@@ -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<Types.NodeLocation> {
|
||||
const { location } = this;
|
||||
|
||||
return location ? [location.lat, location.lon] : null;
|
||||
}
|
||||
|
||||
public get average(): number {
|
||||
let accounted = 0;
|
||||
let sum = 0;
|
||||
|
||||
@@ -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<string, Location>();
|
||||
|
||||
export async function locate(ip: string): Promise<Maybe<Location>> {
|
||||
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<Maybe<Location>>((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);
|
||||
});
|
||||
})
|
||||
}
|
||||
+22
-12
@@ -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<NodeLocation>];
|
||||
}
|
||||
|
||||
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
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -16,7 +16,10 @@ export type PropagationTime = Opaque<Milliseconds, 'PropagationTime'>;
|
||||
export type NodeCount = Opaque<number, 'NodeCount'>;
|
||||
export type PeerCount = Opaque<number, 'PeerCount'>;
|
||||
export type TransactionCount = Opaque<number, 'TransactionCount'>;
|
||||
export type Latitude = Opaque<number, 'Latitude'>;
|
||||
export type Longitude = Opaque<number, 'Longitude'>;
|
||||
|
||||
export type BlockDetails = [BlockNumber, BlockHash, Milliseconds, Timestamp, Maybe<PropagationTime>];
|
||||
export type NodeDetails = [NodeName, NodeImplementation, NodeVersion];
|
||||
export type NodeStats = [PeerCount, TransactionCount];
|
||||
export type NodeLocation = [Latitude, Longitude];
|
||||
|
||||
@@ -8,7 +8,7 @@
|
||||
<title>Polkadot Telemetry</title>
|
||||
<style>
|
||||
body, html {
|
||||
background: #eee;
|
||||
background: #fff;
|
||||
color: #111;
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -50,7 +50,7 @@ export default class App extends React.Component<{}, State> {
|
||||
<div className="App">
|
||||
<OfflineIndicator status={status} />
|
||||
<Chains chains={chains} subscribed={subscribed} connection={this.connection} />
|
||||
<Chain state={this.state} />
|
||||
<Chain appState={this.state} />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
|
||||
File diff suppressed because one or more lines are too long
|
After Width: | Height: | Size: 84 KiB |
File diff suppressed because one or more lines are too long
|
After Width: | Height: | Size: 20 KiB |
@@ -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%;
|
||||
|
||||
@@ -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<State>
|
||||
appState: Readonly<AppState>;
|
||||
}
|
||||
|
||||
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<Chain.Props, Chain.State> {
|
||||
constructor(props: Chain.Props) {
|
||||
super(props);
|
||||
|
||||
const nodes = Array.from(props.state.nodes.values()).sort(sortNodes);
|
||||
this.state = {
|
||||
display: 'table'
|
||||
};
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="Chain">
|
||||
<div className="Chain-header">
|
||||
<Tile icon={blockIcon} title="Best Block">#{formatNumber(best)}</Tile>
|
||||
<Tile icon={blockTimeIcon} title="Avgerage Time">{ blockAverage == null ? '-' : (blockAverage / 1000).toFixed(3) + 's' }</Tile>
|
||||
<Tile icon={lastTimeIcon} title="Last Block"><Ago when={blockTimestamp} /></Tile>
|
||||
</div>
|
||||
<div className="Chain-content-container">
|
||||
<div className="Chain-content">
|
||||
<table className="Chain-node-list">
|
||||
<thead>
|
||||
<tr>
|
||||
<th><Icon src={nodeIcon} alt="Node" /></th>
|
||||
<th><Icon src={nodeTypeIcon} alt="Implementation" /></th>
|
||||
<th><Icon src={peersIcon} alt="Peer Count" /></th>
|
||||
<th><Icon src={transactionsIcon} alt="Transactions in Queue" /></th>
|
||||
<th><Icon src={blockIcon} alt="Block" /></th>
|
||||
<th><Icon src={blockHashIcon} alt="Block Hash" /></th>
|
||||
<th><Icon src={blockTimeIcon} alt="Block Time" /></th>
|
||||
<th><Icon src={propagationTimeIcon} alt="Block Propagation Time" /></th>
|
||||
<th><Icon src={lastTimeIcon} alt="Last Block Time" /></th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{
|
||||
nodes.map((node) => <Node key={node.id} {...node} />)
|
||||
}
|
||||
</tbody>
|
||||
</table>
|
||||
public render() {
|
||||
const { best, blockTimestamp, blockAverage } = this.props.appState;
|
||||
|
||||
return (
|
||||
<div className="Chain">
|
||||
<div className="Chain-header">
|
||||
<Tile icon={blockIcon} title="Best Block">#{formatNumber(best)}</Tile>
|
||||
<Tile icon={blockTimeIcon} title="Avgerage Time">{ blockAverage == null ? '-' : (blockAverage / 1000).toFixed(3) + 's' }</Tile>
|
||||
<Tile icon={lastTimeIcon} title="Last Block"><Ago when={blockTimestamp} /></Tile>
|
||||
</div>
|
||||
<div className="Chain-content-container">
|
||||
<div className="Chain-content">
|
||||
{
|
||||
this.state.display === 'table'
|
||||
? this.renderTable()
|
||||
: this.renderMap()
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
);
|
||||
}
|
||||
|
||||
private renderMap() {
|
||||
// return <ReactSVG path={worldMap} className="Chain-map" />;
|
||||
return (
|
||||
<div className="Chain-map">
|
||||
{
|
||||
this.nodes().map((node) => <div key={node.id} className="Chain-map-node" data-foo={JSON.stringify(node)} />)
|
||||
}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
private renderTable() {
|
||||
return (
|
||||
<table className="Chain-node-list">
|
||||
<thead>
|
||||
<tr>
|
||||
<th><Icon src={nodeIcon} alt="Node" /></th>
|
||||
<th><Icon src={nodeTypeIcon} alt="Implementation" /></th>
|
||||
<th><Icon src={peersIcon} alt="Peer Count" /></th>
|
||||
<th><Icon src={transactionsIcon} alt="Transactions in Queue" /></th>
|
||||
<th><Icon src={blockIcon} alt="Block" /></th>
|
||||
<th><Icon src={blockHashIcon} alt="Block Hash" /></th>
|
||||
<th><Icon src={blockTimeIcon} alt="Block Time" /></th>
|
||||
<th><Icon src={propagationTimeIcon} alt="Block Propagation Time" /></th>
|
||||
<th><Icon src={lastTimeIcon} alt="Last Block Time" /></th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{
|
||||
this.nodes().sort(sortNodes).map((node) => <Node key={node.id} {...node} />)
|
||||
}
|
||||
</tbody>
|
||||
</table>
|
||||
);
|
||||
}
|
||||
|
||||
private nodes() {
|
||||
return Array.from(this.props.appState.nodes.values());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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<Types.NodeLocation>,
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user