diff --git a/packages/frontend/src/App.tsx b/packages/frontend/src/App.tsx index 72762b8..cd722e7 100644 --- a/packages/frontend/src/App.tsx +++ b/packages/frontend/src/App.tsx @@ -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 = { diff --git a/packages/frontend/src/Connection.ts b/packages/frontend/src/Connection.ts index 6cfc5be..792f3c2 100644 --- a/packages/frontend/src/Connection.ts +++ b/packages/frontend/src/Connection.ts @@ -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; } diff --git a/packages/frontend/src/components/Chain/Chain.tsx b/packages/frontend/src/components/Chain/Chain.tsx index a3b8a6f..0a9ac5b 100644 --- a/packages/frontend/src/components/Chain/Chain.tsx +++ b/packages/frontend/src/components/Chain/Chain.tsx @@ -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 {
{ 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 ( @@ -200,7 +200,7 @@ export class Chain extends React.Component { ); } - private nodes(): AppState.Node[] { + private nodes(): NodeState[] { return this.props.appState.sortedNodes; } @@ -271,7 +271,7 @@ export class Chain extends React.Component { 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 { 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; diff --git a/packages/frontend/src/components/Node/Location.tsx b/packages/frontend/src/components/Node/Location.tsx index 13eb682..7d03512 100644 --- a/packages/frontend/src/components/Node/Location.tsx +++ b/packages/frontend/src/components/Node/Location.tsx @@ -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 { 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 { return (
{ - this.state.hover ? this.renderDetails(location) : null + this.state.hover ? this.renderDetails() : null }
); } - 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 { {secondsWithPrecision(blockTime/1000)} - {propagationTime === null ? '∞' : milliOrSecond(propagationTime as number)} + {propagationTime == null ? '∞' : milliOrSecond(propagationTime)} diff --git a/packages/frontend/src/components/Node/Row.tsx b/packages/frontend/src/components/Node/Row.tsx index 54b62c8..ff68f0c 100644 --- a/packages/frontend/src/components/Node/Row.tsx +++ b/packages/frontend/src/components/Node/Row.tsx @@ -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; }; @@ -42,7 +42,7 @@ interface Column { icon: string; width?: number; setting?: keyof AppState.Settings; - render: (node: AppState.Node) => React.ReactElement | string; + render: (node: Node) => React.ReactElement | string; } function Truncate(props: { text: string }): React.ReactElement { @@ -56,16 +56,14 @@ export default class Row extends React.Component { { label: 'Node', icon: nodeIcon, - render: ({ nodeDetails }) => + render: ({ name }) => }, { label: 'Validator', icon: nodeValidatorIcon, width: 26, setting: 'validator', - render: ({ nodeDetails }) => { - const validator = nodeDetails[3]; - + render: ({ validator }) => { return validator ? : '-'; } }, @@ -74,15 +72,14 @@ export default class Row extends React.Component { icon: nodeLocationIcon, width: 140, setting: 'location', - render: ({ location }) => location && location[2] ? : '-' + render: ({ city }) => 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 { 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 ? {memory / 1024 | 0}mb : '-'; - } + render: ({ mem }) => mem ? {mem / 1024 | 0}mb : '-' }, { 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 }) => + render: ({ 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 }) => + render: ({ blockTimestamp }) => }, ]; @@ -188,11 +173,10 @@ export default class Row extends React.Component { 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 { 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); } } } diff --git a/packages/frontend/src/state.ts b/packages/frontend/src/state.ts index ffd3a9f..37a9859 100644 --- a/packages/frontend/src/state.ts +++ b/packages/frontend/src/state.ts @@ -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; +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; + + public pinned: boolean; + public peers: Types.PeerCount; + public txs: Types.TransactionCount; + public mem: Maybe; + public cpu: Maybe; + + public height: Types.BlockNumber; + public hash: Types.BlockHash; + public blockTime: Types.Milliseconds; + public blockTimestamp: Types.Timestamp; + public propagationTime: Maybe; + + public lat: Maybe; + public lon: Maybe; + public city: Maybe; + + constructor( + pinned: boolean, + id: Types.NodeId, + nodeDetails: Types.NodeDetails, + nodeStats: Types.NodeStats, + blockDetails: Types.BlockDetails, + location: Maybe + ) { + 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; chains: Map; - nodes: Map; - sortedNodes: State.Node[]; + nodes: Map; + sortedNodes: Node[]; settings: Readonly; pins: Readonly>; } export type Update = (changes: Pick | null) => Readonly; -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]; -} diff --git a/packages/frontend/src/utils.ts b/packages/frontend/src/utils.ts index 82fd9ce..e112053 100644 --- a/packages/frontend/src/utils.ts +++ b/packages/frontend/src/utils.ts @@ -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`; }