mirror of
https://github.com/pezkuwichain/pezkuwi-telemetry.git
synced 2026-05-31 00:01:06 +00:00
Demystify Node state (#61)
This commit is contained in:
@@ -3,7 +3,7 @@ import { Types } from '@dotstats/common';
|
||||
import { Chains, Chain, Ago, OfflineIndicator } from './components';
|
||||
import { Connection } from './Connection';
|
||||
import { PersistentObject, PersistentSet } from './persist';
|
||||
import { State, compareNodes } from './state';
|
||||
import { State, Node } from './state';
|
||||
|
||||
import './App.css';
|
||||
|
||||
@@ -39,10 +39,10 @@ export default class App extends React.Component<{}, State> {
|
||||
const { nodes, sortedNodes } = this.state;
|
||||
|
||||
for (const node of nodes.values()) {
|
||||
node.pinned = pins.has(node.nodeDetails[0]);
|
||||
node.pinned = pins.has(node.name);
|
||||
}
|
||||
|
||||
this.setState({ nodes, pins, sortedNodes: sortedNodes.sort(compareNodes) });
|
||||
this.setState({ nodes, pins, sortedNodes: sortedNodes.sort(Node.compare) });
|
||||
});
|
||||
|
||||
this.state = {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { VERSION, timestamp, FeedMessage, Types, Maybe, sleep } from '@dotstats/common';
|
||||
import { sortedInsert, sortedIndexOf } from '@dotstats/common';
|
||||
import { State, Update, compareNodes } from './state';
|
||||
import { State, Update, Node } from './state';
|
||||
import { PersistentSet } from './persist';
|
||||
|
||||
const { Actions } = FeedMessage;
|
||||
@@ -163,7 +163,7 @@ export class Connection {
|
||||
case Actions.BestBlock: {
|
||||
const [best, blockTimestamp, blockAverage] = message.payload;
|
||||
|
||||
nodes.forEach((node) => node.blockDetails[4] = null);
|
||||
nodes.forEach((node) => node.propagationTime = null);
|
||||
|
||||
this.state = this.update({ best, blockTimestamp, blockAverage });
|
||||
|
||||
@@ -173,10 +173,10 @@ export class Connection {
|
||||
case Actions.AddedNode: {
|
||||
const [id, nodeDetails, nodeStats, blockDetails, location] = message.payload;
|
||||
const pinned = this.pins.has(nodeDetails[0]);
|
||||
const node = { pinned, id, nodeDetails, nodeStats, blockDetails, location };
|
||||
const node = new Node(pinned, id, nodeDetails, nodeStats, blockDetails, location);
|
||||
|
||||
nodes.set(id, node);
|
||||
sortedInsert(node, sortedNodes, compareNodes);
|
||||
sortedInsert(node, sortedNodes, Node.compare);
|
||||
|
||||
if (nodes.size !== sortedNodes.length) {
|
||||
console.error('Node count in sorted array is wrong!');
|
||||
@@ -191,7 +191,7 @@ export class Connection {
|
||||
|
||||
if (node) {
|
||||
nodes.delete(id);
|
||||
const index = sortedIndexOf(node, sortedNodes, compareNodes);
|
||||
const index = sortedIndexOf(node, sortedNodes, Node.compare);
|
||||
sortedNodes.splice(index, 1);
|
||||
|
||||
if (nodes.size !== sortedNodes.length) {
|
||||
@@ -205,14 +205,16 @@ export class Connection {
|
||||
}
|
||||
|
||||
case Actions.LocatedNode: {
|
||||
const [id, latitude, longitude, city] = message.payload;
|
||||
const [id, lat, lon, city] = message.payload;
|
||||
const node = nodes.get(id);
|
||||
|
||||
if (!node) {
|
||||
continue messages;
|
||||
}
|
||||
|
||||
node.location = [latitude, longitude, city];
|
||||
node.lat = lat;
|
||||
node.lon = lon;
|
||||
node.city = city;
|
||||
|
||||
break;
|
||||
}
|
||||
@@ -225,8 +227,8 @@ export class Connection {
|
||||
continue messages;
|
||||
}
|
||||
|
||||
node.blockDetails = blockDetails;
|
||||
sortedNodes = sortedNodes.sort(compareNodes);
|
||||
node.updateBlock(blockDetails);
|
||||
sortedNodes = sortedNodes.sort(Node.compare);
|
||||
|
||||
break;
|
||||
}
|
||||
@@ -239,7 +241,7 @@ export class Connection {
|
||||
return;
|
||||
}
|
||||
|
||||
node.nodeStats = nodeStats;
|
||||
node.updateStats(nodeStats);
|
||||
|
||||
break;
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import * as React from 'react';
|
||||
import { Types, Maybe } from '@dotstats/common';
|
||||
import { State as AppState } from '../../state';
|
||||
import { State as AppState, Node as NodeState } from '../../state';
|
||||
import { formatNumber, secondsWithPrecision, viewport } from '../../utils';
|
||||
import { Tab, Filter } from './';
|
||||
import { Tile, Node, Ago, Setting } from '../';
|
||||
@@ -158,15 +158,15 @@ export class Chain extends React.Component<Chain.Props, Chain.State> {
|
||||
<div className="Chain-map">
|
||||
{
|
||||
this.nodes().map((node) => {
|
||||
const location = node.location;
|
||||
const { lat, lon } = node;
|
||||
const focused = nodeFilter == null || nodeFilter(node);
|
||||
|
||||
if (!location || location[0] == null || location[1] == null) {
|
||||
if (lat == null || lon == null) {
|
||||
// Skip nodes with unknown location
|
||||
return null;
|
||||
}
|
||||
|
||||
const position = this.pixelPosition(location[0], location[1]);
|
||||
const position = this.pixelPosition(lat, lon);
|
||||
|
||||
return (
|
||||
<Node.Location key={node.id} position={position} focused={focused} node={node} />
|
||||
@@ -200,7 +200,7 @@ export class Chain extends React.Component<Chain.Props, Chain.State> {
|
||||
);
|
||||
}
|
||||
|
||||
private nodes(): AppState.Node[] {
|
||||
private nodes(): NodeState[] {
|
||||
return this.props.appState.sortedNodes;
|
||||
}
|
||||
|
||||
@@ -271,7 +271,7 @@ export class Chain extends React.Component<Chain.Props, Chain.State> {
|
||||
this.setState({ filter });
|
||||
}
|
||||
|
||||
private getNodeFilter(): Maybe<(node: AppState.Node) => boolean> {
|
||||
private getNodeFilter(): Maybe<(node: NodeState) => boolean> {
|
||||
const { filter } = this.state;
|
||||
|
||||
if (filter == null) {
|
||||
@@ -280,9 +280,8 @@ export class Chain extends React.Component<Chain.Props, Chain.State> {
|
||||
|
||||
const filterLC = filter.toLowerCase();
|
||||
|
||||
return ({ nodeDetails, location }) => {
|
||||
const city = location && location[2];
|
||||
const matchesName = nodeDetails[0].toLowerCase().indexOf(filterLC) !== -1;
|
||||
return ({ name, city }) => {
|
||||
const matchesName = name.toLowerCase().indexOf(filterLC) !== -1;
|
||||
const matchesCity = city != null && city.toLowerCase().indexOf(filterLC) !== -1;
|
||||
|
||||
return matchesName || matchesCity;
|
||||
|
||||
@@ -1,9 +1,8 @@
|
||||
import * as React from 'react';
|
||||
import Identicon from 'polkadot-identicon';
|
||||
import { Types } from '@dotstats/common';
|
||||
import { formatNumber, trimHash, milliOrSecond, secondsWithPrecision } from '../../utils';
|
||||
import { Ago, Icon } from '../';
|
||||
import { State as AppState } from '../../state';
|
||||
import { Node } from '../../state';
|
||||
|
||||
import nodeIcon from '../../icons/server.svg';
|
||||
import nodeValidatorIcon from '../../icons/shield.svg';
|
||||
@@ -21,7 +20,7 @@ namespace Location {
|
||||
export type Quarter = 0 | 1 | 2 | 3;
|
||||
|
||||
export interface Props {
|
||||
node: AppState.Node;
|
||||
node: Node;
|
||||
position: Position;
|
||||
focused: boolean;
|
||||
}
|
||||
@@ -43,9 +42,7 @@ class Location extends React.Component<Location.Props, Location.State> {
|
||||
public render() {
|
||||
const { node, position, focused } = this.props;
|
||||
const { left, top, quarter } = position;
|
||||
const { blockDetails, location } = node;
|
||||
const height = blockDetails[0];
|
||||
const propagationTime = blockDetails[4];
|
||||
const { height, propagationTime } = node;
|
||||
|
||||
if (!location) {
|
||||
return null;
|
||||
@@ -66,17 +63,25 @@ class Location extends React.Component<Location.Props, Location.State> {
|
||||
return (
|
||||
<div className={className} style={{ left, top }} onMouseOver={this.onMouseOver} onMouseOut={this.onMouseOut}>
|
||||
{
|
||||
this.state.hover ? this.renderDetails(location) : null
|
||||
this.state.hover ? this.renderDetails() : null
|
||||
}
|
||||
<div className="Node-Location-ping" />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
private renderDetails(location: Types.NodeLocation) {
|
||||
const { node } = this.props;
|
||||
const [name, implementation, version, validator] = node.nodeDetails;
|
||||
const [height, hash, blockTime, blockTimestamp, propagationTime] = node.blockDetails;
|
||||
private renderDetails() {
|
||||
const {
|
||||
name,
|
||||
implementation,
|
||||
version,
|
||||
validator,
|
||||
height,
|
||||
hash,
|
||||
blockTime,
|
||||
blockTimestamp,
|
||||
propagationTime
|
||||
} = this.props.node;
|
||||
|
||||
let validatorRow = null;
|
||||
|
||||
@@ -115,7 +120,7 @@ class Location extends React.Component<Location.Props, Location.State> {
|
||||
<td><Icon src={blockTimeIcon} alt="Block Time" /></td>
|
||||
<td style={{ width: 80 }}>{secondsWithPrecision(blockTime/1000)}</td>
|
||||
<td><Icon src={propagationTimeIcon} alt="Block Propagation Time" /></td>
|
||||
<td style={{ width: 58 }}>{propagationTime === null ? '∞' : milliOrSecond(propagationTime as number)}</td>
|
||||
<td style={{ width: 58 }}>{propagationTime == null ? '∞' : milliOrSecond(propagationTime)}</td>
|
||||
<td><Icon src={lastTimeIcon} alt="Last Block Time" /></td>
|
||||
<td style={{ minWidth: 82 }}><Ago when={blockTimestamp} /></td>
|
||||
</tr>
|
||||
|
||||
@@ -2,7 +2,7 @@ import * as React from 'react';
|
||||
import Identicon from 'polkadot-identicon';
|
||||
import { Types } from '@dotstats/common';
|
||||
import { formatNumber, milliOrSecond, secondsWithPrecision } from '../../utils';
|
||||
import { State as AppState } from '../../state';
|
||||
import { State as AppState, Node } from '../../state';
|
||||
import { PersistentSet } from '../../persist';
|
||||
import { SEMVER_PATTERN } from './';
|
||||
import { Ago, Icon } from '../';
|
||||
@@ -28,7 +28,7 @@ import unknownImplementationIcon from '../../icons/question-solid.svg';
|
||||
import './Row.css';
|
||||
|
||||
interface RowProps {
|
||||
node: AppState.Node;
|
||||
node: Node;
|
||||
settings: AppState.Settings;
|
||||
pins: PersistentSet<Types.NodeName>;
|
||||
};
|
||||
@@ -42,7 +42,7 @@ interface Column {
|
||||
icon: string;
|
||||
width?: number;
|
||||
setting?: keyof AppState.Settings;
|
||||
render: (node: AppState.Node) => React.ReactElement<any> | string;
|
||||
render: (node: Node) => React.ReactElement<any> | string;
|
||||
}
|
||||
|
||||
function Truncate(props: { text: string }): React.ReactElement<any> {
|
||||
@@ -56,16 +56,14 @@ export default class Row extends React.Component<RowProps, {}> {
|
||||
{
|
||||
label: 'Node',
|
||||
icon: nodeIcon,
|
||||
render: ({ nodeDetails }) => <Truncate text={nodeDetails[0]} />
|
||||
render: ({ name }) => <Truncate text={name} />
|
||||
},
|
||||
{
|
||||
label: 'Validator',
|
||||
icon: nodeValidatorIcon,
|
||||
width: 26,
|
||||
setting: 'validator',
|
||||
render: ({ nodeDetails }) => {
|
||||
const validator = nodeDetails[3];
|
||||
|
||||
render: ({ validator }) => {
|
||||
return validator ? <span className="Node-Row-validator" title={validator}><Identicon id={validator} size={16} /></span> : '-';
|
||||
}
|
||||
},
|
||||
@@ -74,15 +72,14 @@ export default class Row extends React.Component<RowProps, {}> {
|
||||
icon: nodeLocationIcon,
|
||||
width: 140,
|
||||
setting: 'location',
|
||||
render: ({ location }) => location && location[2] ? <Truncate text={location[2]} /> : '-'
|
||||
render: ({ city }) => city ? <Truncate text={city} /> : '-'
|
||||
},
|
||||
{
|
||||
label: 'Implementation',
|
||||
icon: nodeTypeIcon,
|
||||
width: 90,
|
||||
setting: 'implementation',
|
||||
render: ({ nodeDetails }) => {
|
||||
const [, implementation, version] = nodeDetails;
|
||||
render: ({ implementation, version }) => {
|
||||
const [semver] = version.match(SEMVER_PATTERN) || [version];
|
||||
const implIcon = implementation === 'parity-polkadot' ? parityPolkadotIcon
|
||||
: implementation === 'substrate-node' ? paritySubstrateIcon
|
||||
@@ -96,75 +93,63 @@ export default class Row extends React.Component<RowProps, {}> {
|
||||
icon: peersIcon,
|
||||
width: 26,
|
||||
setting: 'peers',
|
||||
render: ({ nodeStats }) => `${nodeStats[0]}`
|
||||
render: ({ peers }) => `${peers}`
|
||||
},
|
||||
{
|
||||
label: 'Transactions in Queue',
|
||||
icon: transactionsIcon,
|
||||
width: 26,
|
||||
setting: 'txs',
|
||||
render: ({ nodeStats }) => `${nodeStats[1]}`
|
||||
render: ({ txs }) => `${txs}`
|
||||
},
|
||||
{
|
||||
label: '% CPU Use',
|
||||
icon: cpuIcon,
|
||||
width: 26,
|
||||
setting: 'cpu',
|
||||
render: ({ nodeStats }) => {
|
||||
const cpu = nodeStats[3];
|
||||
|
||||
return cpu ? `${cpu.toFixed(1)}%` : '-';
|
||||
}
|
||||
render: ({ cpu }) => cpu ? `${cpu.toFixed(1)}%` : '-'
|
||||
},
|
||||
{
|
||||
label: 'Memory Use',
|
||||
icon: memoryIcon,
|
||||
width: 26,
|
||||
setting: 'mem',
|
||||
render: ({ nodeStats }) => {
|
||||
const memory = nodeStats[2];
|
||||
|
||||
return memory ? <span title={`${memory}kb`}>{memory / 1024 | 0}mb</span> : '-';
|
||||
}
|
||||
render: ({ mem }) => mem ? <span title={`${mem}kb`}>{mem / 1024 | 0}mb</span> : '-'
|
||||
},
|
||||
{
|
||||
label: 'Block',
|
||||
icon: blockIcon,
|
||||
width: 88,
|
||||
setting: 'blocknumber',
|
||||
render: ({ blockDetails }) => `#${formatNumber(blockDetails[0])}`
|
||||
render: ({ height }) => `#${formatNumber(height)}`
|
||||
},
|
||||
{
|
||||
label: 'Block Hash',
|
||||
icon: blockHashIcon,
|
||||
width: 154,
|
||||
setting: 'blockhash',
|
||||
render: ({ blockDetails }) => <Truncate text={blockDetails[1]} />
|
||||
render: ({ hash }) => <Truncate text={hash} />
|
||||
},
|
||||
{
|
||||
label: 'Block Time',
|
||||
icon: blockTimeIcon,
|
||||
width: 80,
|
||||
setting: 'blocktime',
|
||||
render: ({ blockDetails }) => `${secondsWithPrecision(blockDetails[2]/1000)}`
|
||||
render: ({ blockTime }) => `${secondsWithPrecision(blockTime/1000)}`
|
||||
},
|
||||
{
|
||||
label: 'Block Propagation Time',
|
||||
icon: propagationTimeIcon,
|
||||
width: 58,
|
||||
setting: 'blockpropagation',
|
||||
render: ({ blockDetails }) => {
|
||||
const propagationTime = blockDetails[4];
|
||||
|
||||
return propagationTime === null ? '∞' : milliOrSecond(propagationTime as number);
|
||||
}
|
||||
render: ({ propagationTime }) => propagationTime == null ? '∞' : milliOrSecond(propagationTime)
|
||||
},
|
||||
{
|
||||
label: 'Last Block Time',
|
||||
icon: lastTimeIcon,
|
||||
width: 100,
|
||||
setting: 'blocklasttime',
|
||||
render: ({ blockDetails }) => <Ago when={blockDetails[3]} />
|
||||
render: ({ blockTimestamp }) => <Ago when={blockTimestamp} />
|
||||
},
|
||||
];
|
||||
|
||||
@@ -188,11 +173,10 @@ export default class Row extends React.Component<RowProps, {}> {
|
||||
|
||||
public render() {
|
||||
const { node, settings } = this.props;
|
||||
const propagationTime = node.blockDetails[4];
|
||||
|
||||
let className = 'Node-Row';
|
||||
|
||||
if (propagationTime != null) {
|
||||
if (node.propagationTime != null) {
|
||||
className += ' Node-Row-synced';
|
||||
}
|
||||
|
||||
@@ -213,12 +197,11 @@ export default class Row extends React.Component<RowProps, {}> {
|
||||
|
||||
public toggle = () => {
|
||||
const { pins, node } = this.props;
|
||||
const name = node.nodeDetails[0];
|
||||
|
||||
if (node.pinned) {
|
||||
pins.delete(name)
|
||||
pins.delete(node.name)
|
||||
} else {
|
||||
pins.add(name);
|
||||
pins.add(node.name);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,15 +1,100 @@
|
||||
import { Types, Maybe } from '@dotstats/common';
|
||||
|
||||
export namespace State {
|
||||
export interface Node {
|
||||
pinned: boolean,
|
||||
id: Types.NodeId;
|
||||
nodeDetails: Types.NodeDetails;
|
||||
nodeStats: Types.NodeStats;
|
||||
blockDetails: Types.BlockDetails;
|
||||
location: Maybe<Types.NodeLocation>;
|
||||
export class Node {
|
||||
public static compare(a: Node, b: Node): number {
|
||||
if (a.pinned === b.pinned) {
|
||||
if (a.height === b.height) {
|
||||
const aPropagation = a.propagationTime == null ? Infinity : a.propagationTime as number;
|
||||
const bPropagation = b.propagationTime == null ? Infinity : b.propagationTime as number;
|
||||
|
||||
// Ascending sort by propagation time
|
||||
return aPropagation - bPropagation;
|
||||
}
|
||||
} else {
|
||||
return +b.pinned - +a.pinned;
|
||||
}
|
||||
|
||||
// Descending sort by block number
|
||||
return b.height - a.height;
|
||||
}
|
||||
|
||||
public readonly id: Types.NodeId;
|
||||
public readonly name: Types.NodeName;
|
||||
public readonly implementation: Types.NodeImplementation;
|
||||
public readonly version: Types.NodeVersion;
|
||||
public readonly validator: Maybe<Types.Address>;
|
||||
|
||||
public pinned: boolean;
|
||||
public peers: Types.PeerCount;
|
||||
public txs: Types.TransactionCount;
|
||||
public mem: Maybe<Types.MemoryUse>;
|
||||
public cpu: Maybe<Types.CPUUse>;
|
||||
|
||||
public height: Types.BlockNumber;
|
||||
public hash: Types.BlockHash;
|
||||
public blockTime: Types.Milliseconds;
|
||||
public blockTimestamp: Types.Timestamp;
|
||||
public propagationTime: Maybe<Types.PropagationTime>;
|
||||
|
||||
public lat: Maybe<Types.Latitude>;
|
||||
public lon: Maybe<Types.Longitude>;
|
||||
public city: Maybe<Types.City>;
|
||||
|
||||
constructor(
|
||||
pinned: boolean,
|
||||
id: Types.NodeId,
|
||||
nodeDetails: Types.NodeDetails,
|
||||
nodeStats: Types.NodeStats,
|
||||
blockDetails: Types.BlockDetails,
|
||||
location: Maybe<Types.NodeLocation>
|
||||
) {
|
||||
const [name, implementation, version, validator] = nodeDetails;
|
||||
|
||||
this.pinned = pinned;
|
||||
|
||||
this.id = id;
|
||||
this.name = name;
|
||||
this.implementation = implementation;
|
||||
this.version = version;
|
||||
this.validator = validator;
|
||||
|
||||
this.updateStats(nodeStats);
|
||||
this.updateBlock(blockDetails);
|
||||
|
||||
if (location) {
|
||||
this.updateLocation(location);
|
||||
}
|
||||
}
|
||||
|
||||
public updateStats(stats: Types.NodeStats) {
|
||||
const [peers, txs, mem, cpu] = stats;
|
||||
|
||||
this.peers = peers;
|
||||
this.txs = txs;
|
||||
this.mem = mem;
|
||||
this.cpu = cpu;
|
||||
}
|
||||
|
||||
public updateBlock(block: Types.BlockDetails) {
|
||||
const [height, hash, blockTime, blockTimestamp, propagationTime] = block;
|
||||
|
||||
this.height = height;
|
||||
this.hash = hash;
|
||||
this.blockTime = blockTime;
|
||||
this.blockTimestamp = blockTimestamp;
|
||||
this.propagationTime = propagationTime;
|
||||
}
|
||||
|
||||
public updateLocation(location: Types.NodeLocation) {
|
||||
const [lat, lon, city] = location;
|
||||
|
||||
this.lat = lat;
|
||||
this.lon = lon;
|
||||
this.city = city;
|
||||
}
|
||||
}
|
||||
|
||||
export namespace State {
|
||||
export interface Settings {
|
||||
location: boolean;
|
||||
validator: boolean;
|
||||
@@ -34,27 +119,11 @@ export interface State {
|
||||
timeDiff: Types.Milliseconds;
|
||||
subscribed: Maybe<Types.ChainLabel>;
|
||||
chains: Map<Types.ChainLabel, Types.NodeCount>;
|
||||
nodes: Map<Types.NodeId, State.Node>;
|
||||
sortedNodes: State.Node[];
|
||||
nodes: Map<Types.NodeId, Node>;
|
||||
sortedNodes: Node[];
|
||||
settings: Readonly<State.Settings>;
|
||||
pins: Readonly<Set<Types.NodeName>>;
|
||||
}
|
||||
|
||||
export type Update = <K extends keyof State>(changes: Pick<State, K> | null) => Readonly<State>;
|
||||
|
||||
export function compareNodes(a: State.Node, b: State.Node): number {
|
||||
if (a.pinned === b.pinned) {
|
||||
if (a.blockDetails[0] === b.blockDetails[0]) {
|
||||
const aPropagation = a.blockDetails[4] == null ? Infinity : a.blockDetails[4] as number;
|
||||
const bPropagation = b.blockDetails[4] == null ? Infinity : b.blockDetails[4] as number;
|
||||
|
||||
// Ascending sort by propagation time
|
||||
return aPropagation - bPropagation;
|
||||
}
|
||||
} else {
|
||||
return +b.pinned - +a.pinned;
|
||||
}
|
||||
|
||||
// Descending sort by block number
|
||||
return b.blockDetails[0] - a.blockDetails[0];
|
||||
}
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
import { Types } from '@dotstats/common';
|
||||
|
||||
export interface Viewport {
|
||||
width: number;
|
||||
height: number;
|
||||
@@ -36,7 +38,7 @@ export function trimHash(hash: string, length: number): string {
|
||||
return hash.substr(0, side) + '..' + hash.substr(-side, side);
|
||||
}
|
||||
|
||||
export function milliOrSecond(num: number): string {
|
||||
export function milliOrSecond(num: Types.Milliseconds | Types.PropagationTime): string {
|
||||
if (num < 10000) {
|
||||
return `${num}ms`;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user