mirror of
https://github.com/pezkuwichain/pezkuwi-telemetry.git
synced 2026-06-12 22:41:01 +00:00
Offline indicator, average block time and stuff
This commit is contained in:
@@ -23,6 +23,8 @@ export default class Aggregator {
|
|||||||
public addFeed(feed: Feed) {
|
public addFeed(feed: Feed) {
|
||||||
this.feeds.add(feed);
|
this.feeds.add(feed);
|
||||||
|
|
||||||
|
feed.sendMessage(Feed.feedVersion());
|
||||||
|
|
||||||
for (const chain of this.chains.values()) {
|
for (const chain of this.chains.values()) {
|
||||||
feed.sendMessage(Feed.addedChain(chain));
|
feed.sendMessage(Feed.addedChain(chain));
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,7 +2,9 @@ import * as EventEmitter from 'events';
|
|||||||
import Node from './Node';
|
import Node from './Node';
|
||||||
import Feed from './Feed';
|
import Feed from './Feed';
|
||||||
import FeedSet from './FeedSet';
|
import FeedSet from './FeedSet';
|
||||||
import { timestamp, Types, FeedMessage } from '@dotstats/common';
|
import { timestamp, Maybe, Types, FeedMessage } from '@dotstats/common';
|
||||||
|
|
||||||
|
const BLOCK_TIME_HISTORY = 10;
|
||||||
|
|
||||||
export default class Chain {
|
export default class Chain {
|
||||||
private nodes = new Set<Node>();
|
private nodes = new Set<Node>();
|
||||||
@@ -14,6 +16,8 @@ export default class Chain {
|
|||||||
public height = 0 as Types.BlockNumber;
|
public height = 0 as Types.BlockNumber;
|
||||||
public blockTimestamp = 0 as Types.Timestamp;
|
public blockTimestamp = 0 as Types.Timestamp;
|
||||||
|
|
||||||
|
private blockTimes: Array<number> = new Array(BLOCK_TIME_HISTORY);
|
||||||
|
|
||||||
constructor(label: Types.ChainLabel) {
|
constructor(label: Types.ChainLabel) {
|
||||||
this.label = label;
|
this.label = label;
|
||||||
}
|
}
|
||||||
@@ -48,7 +52,7 @@ export default class Chain {
|
|||||||
feed.chain = this.label;
|
feed.chain = this.label;
|
||||||
|
|
||||||
feed.sendMessage(Feed.timeSync());
|
feed.sendMessage(Feed.timeSync());
|
||||||
feed.sendMessage(Feed.bestBlock(this.height, this.blockTimestamp));
|
feed.sendMessage(Feed.bestBlock(this.height, this.blockTimestamp, this.averageBlockTime));
|
||||||
|
|
||||||
for (const node of this.nodes.values()) {
|
for (const node of this.nodes.values()) {
|
||||||
feed.sendMessage(Feed.addedNode(node));
|
feed.sendMessage(Feed.addedNode(node));
|
||||||
@@ -75,11 +79,17 @@ export default class Chain {
|
|||||||
|
|
||||||
private updateBlock(node: Node) {
|
private updateBlock(node: Node) {
|
||||||
if (node.height > this.height) {
|
if (node.height > this.height) {
|
||||||
this.height = node.height;
|
const { height, blockTimestamp } = node;
|
||||||
this.blockTimestamp = node.blockTimestamp;
|
|
||||||
|
if (this.blockTimestamp) {
|
||||||
|
this.blockTimes[height * BLOCK_TIME_HISTORY] = blockTimestamp - this.blockTimestamp;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.height = height;
|
||||||
|
this.blockTimestamp = blockTimestamp;
|
||||||
node.propagationTime = 0 as Types.PropagationTime;
|
node.propagationTime = 0 as Types.PropagationTime;
|
||||||
|
|
||||||
this.feeds.broadcast(Feed.bestBlock(this.height, this.blockTimestamp));
|
this.feeds.broadcast(Feed.bestBlock(this.height, this.blockTimestamp, this.averageBlockTime));
|
||||||
|
|
||||||
console.log(`[${this.label}] New block ${this.height}`);
|
console.log(`[${this.label}] New block ${this.height}`);
|
||||||
} else if (node.height === this.height) {
|
} else if (node.height === this.height) {
|
||||||
@@ -90,4 +100,22 @@ export default class Chain {
|
|||||||
|
|
||||||
console.log(`[${this.label}] ${node.name} imported ${node.height}, block time: ${node.blockTime / 1000}s, average: ${node.average / 1000}s | latency ${node.latency}`);
|
console.log(`[${this.label}] ${node.name} imported ${node.height}, block time: ${node.blockTime / 1000}s, average: ${node.average / 1000}s | latency ${node.latency}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private get averageBlockTime(): Maybe<Types.Milliseconds> {
|
||||||
|
let sum = 0;
|
||||||
|
let count = 0;
|
||||||
|
|
||||||
|
for (const time of this.blockTimes) {
|
||||||
|
if (time != null) {
|
||||||
|
sum += time;
|
||||||
|
count += 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (count === 0) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return (sum / count) as Types.Milliseconds;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ import * as WebSocket from 'ws';
|
|||||||
import * as EventEmitter from 'events';
|
import * as EventEmitter from 'events';
|
||||||
import Node from './Node';
|
import Node from './Node';
|
||||||
import Chain from './Chain';
|
import Chain from './Chain';
|
||||||
import { timestamp, Maybe, FeedMessage, Types, idGenerator } from '@dotstats/common';
|
import { VERSION, timestamp, Maybe, FeedMessage, Types, idGenerator } from '@dotstats/common';
|
||||||
|
|
||||||
const nextId = idGenerator<Types.FeedId>();
|
const nextId = idGenerator<Types.FeedId>();
|
||||||
const { Actions } = FeedMessage;
|
const { Actions } = FeedMessage;
|
||||||
@@ -25,10 +25,17 @@ export default class Feed {
|
|||||||
socket.on('close', () => this.disconnect());
|
socket.on('close', () => this.disconnect());
|
||||||
}
|
}
|
||||||
|
|
||||||
public static bestBlock(height: Types.BlockNumber, ts: Types.Timestamp): FeedMessage.Message {
|
public static feedVersion(): FeedMessage.Message {
|
||||||
|
return {
|
||||||
|
action: Actions.FeedVersion,
|
||||||
|
payload: VERSION
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
public static bestBlock(height: Types.BlockNumber, ts: Types.Timestamp, avg: Maybe<Types.Milliseconds>): FeedMessage.Message {
|
||||||
return {
|
return {
|
||||||
action: Actions.BestBlock,
|
action: Actions.BestBlock,
|
||||||
payload: [height, ts]
|
payload: [height, ts, avg]
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
import { Opaque } from './helpers';
|
import { Opaque, Maybe } from './helpers';
|
||||||
import {
|
import {
|
||||||
|
FeedVersion,
|
||||||
NodeId,
|
NodeId,
|
||||||
NodeCount,
|
NodeCount,
|
||||||
NodeDetails,
|
NodeDetails,
|
||||||
@@ -7,10 +8,12 @@ import {
|
|||||||
BlockNumber,
|
BlockNumber,
|
||||||
BlockDetails,
|
BlockDetails,
|
||||||
Timestamp,
|
Timestamp,
|
||||||
|
Milliseconds,
|
||||||
ChainLabel
|
ChainLabel
|
||||||
} from './types';
|
} from './types';
|
||||||
|
|
||||||
export const Actions = {
|
export const Actions = {
|
||||||
|
FeedVersion: 255 as 255,
|
||||||
BestBlock: 0 as 0,
|
BestBlock: 0 as 0,
|
||||||
AddedNode: 1 as 1,
|
AddedNode: 1 as 1,
|
||||||
RemovedNode: 2 as 2,
|
RemovedNode: 2 as 2,
|
||||||
@@ -31,9 +34,14 @@ export namespace Variants {
|
|||||||
action: Action;
|
action: Action;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface FeedVersionMessage extends MessageBase {
|
||||||
|
action: typeof Actions.FeedVersion;
|
||||||
|
payload: FeedVersion;
|
||||||
|
}
|
||||||
|
|
||||||
export interface BestBlockMessage extends MessageBase {
|
export interface BestBlockMessage extends MessageBase {
|
||||||
action: typeof Actions.BestBlock;
|
action: typeof Actions.BestBlock;
|
||||||
payload: [BlockNumber, Timestamp];
|
payload: [BlockNumber, Timestamp, Maybe<Milliseconds>];
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface AddedNodeMessage extends MessageBase {
|
export interface AddedNodeMessage extends MessageBase {
|
||||||
@@ -83,6 +91,7 @@ export namespace Variants {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export type Message =
|
export type Message =
|
||||||
|
| Variants.FeedVersionMessage
|
||||||
| Variants.BestBlockMessage
|
| Variants.BestBlockMessage
|
||||||
| Variants.AddedNodeMessage
|
| Variants.AddedNodeMessage
|
||||||
| Variants.RemovedNodeMessage
|
| Variants.RemovedNodeMessage
|
||||||
|
|||||||
@@ -6,3 +6,5 @@ import * as Types from './types';
|
|||||||
import * as FeedMessage from './feed';
|
import * as FeedMessage from './feed';
|
||||||
|
|
||||||
export { Types, FeedMessage };
|
export { Types, FeedMessage };
|
||||||
|
|
||||||
|
export const VERSION: Types.FeedVersion = 1 as Types.FeedVersion;
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
import { Opaque, Maybe } from './helpers';
|
import { Opaque, Maybe } from './helpers';
|
||||||
import { Id } from './id';
|
import { Id } from './id';
|
||||||
|
|
||||||
|
export type FeedVersion = Opaque<number, 'FeedVersion'>;
|
||||||
export type ChainLabel = Opaque<string, 'ChainLabel'>;
|
export type ChainLabel = Opaque<string, 'ChainLabel'>;
|
||||||
export type FeedId = Id<'Feed'>;
|
export type FeedId = Id<'Feed'>;
|
||||||
export type NodeId = Id<'Node'>;
|
export type NodeId = Id<'Node'>;
|
||||||
|
|||||||
@@ -1,13 +1,14 @@
|
|||||||
.App {
|
.App {
|
||||||
text-align: left;
|
text-align: left;
|
||||||
font-family: Roboto, Helvetica, Arial, sans-serif;
|
font-family: Roboto, Helvetica, Arial, sans-serif;
|
||||||
|
font-size: 16px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.App-no-telemetry {
|
.App-no-telemetry {
|
||||||
width: 100vw;
|
width: 100vw;
|
||||||
height: 100vh;
|
height: 100vh;
|
||||||
line-height: 80vh;
|
line-height: 80vh;
|
||||||
font-size: 3.5em;
|
font-size: 56px;
|
||||||
font-weight: 100;
|
font-weight: 100;
|
||||||
text-align: center;
|
text-align: center;
|
||||||
color: #888;
|
color: #888;
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import * as React from 'react';
|
import * as React from 'react';
|
||||||
import { Types } from '@dotstats/common';
|
import { Types } from '@dotstats/common';
|
||||||
import { Chains, Chain, Ago } from './components';
|
import { Chains, Chain, Ago, OfflineIndicator } from './components';
|
||||||
import { Connection } from './Connection';
|
import { Connection } from './Connection';
|
||||||
import { State } from './state';
|
import { State } from './state';
|
||||||
|
|
||||||
@@ -8,8 +8,10 @@ import './App.css';
|
|||||||
|
|
||||||
export default class App extends React.Component<{}, State> {
|
export default class App extends React.Component<{}, State> {
|
||||||
public state: State = {
|
public state: State = {
|
||||||
|
status: 'offline',
|
||||||
best: 0 as Types.BlockNumber,
|
best: 0 as Types.BlockNumber,
|
||||||
blockTimestamp: 0 as Types.Timestamp,
|
blockTimestamp: 0 as Types.Timestamp,
|
||||||
|
blockAverage: null,
|
||||||
timeDiff: 0 as Types.Milliseconds,
|
timeDiff: 0 as Types.Milliseconds,
|
||||||
subscribed: null,
|
subscribed: null,
|
||||||
chains: new Map(),
|
chains: new Map(),
|
||||||
@@ -31,16 +33,22 @@ export default class App extends React.Component<{}, State> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public render() {
|
public render() {
|
||||||
const { chains, timeDiff, subscribed } = this.state;
|
const { chains, timeDiff, subscribed, status } = this.state;
|
||||||
|
|
||||||
Ago.timeDiff = timeDiff;
|
Ago.timeDiff = timeDiff;
|
||||||
|
|
||||||
if (chains.size === 0) {
|
if (chains.size === 0) {
|
||||||
return <div className="App App-no-telemetry">Waiting for telemetry data...</div>;
|
return (
|
||||||
|
<div className="App App-no-telemetry">
|
||||||
|
<OfflineIndicator status={status} />
|
||||||
|
Waiting for telemetry data...
|
||||||
|
</div>
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="App">
|
<div className="App">
|
||||||
|
<OfflineIndicator status={status} />
|
||||||
<Chains chains={chains} subscribed={subscribed} connection={this.connection} />
|
<Chains chains={chains} subscribed={subscribed} connection={this.connection} />
|
||||||
<Chain state={this.state} />
|
<Chain state={this.state} />
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { timestamp, FeedMessage, Types, Maybe, sleep } from '@dotstats/common';
|
import { VERSION, timestamp, FeedMessage, Types, Maybe, sleep } from '@dotstats/common';
|
||||||
import { State, Update } from './state';
|
import { State, Update } from './state';
|
||||||
|
|
||||||
const { Actions } = FeedMessage;
|
const { Actions } = FeedMessage;
|
||||||
@@ -68,7 +68,10 @@ export class Connection {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private bindSocket() {
|
private bindSocket() {
|
||||||
this.state = this.update({ nodes: new Map() });
|
this.state = this.update({
|
||||||
|
status: 'online',
|
||||||
|
nodes: new Map()
|
||||||
|
});
|
||||||
this.socket.addEventListener('message', this.handleMessages);
|
this.socket.addEventListener('message', this.handleMessages);
|
||||||
this.socket.addEventListener('close', this.handleDisconnect);
|
this.socket.addEventListener('close', this.handleDisconnect);
|
||||||
this.socket.addEventListener('error', this.handleDisconnect);
|
this.socket.addEventListener('error', this.handleDisconnect);
|
||||||
@@ -88,10 +91,24 @@ export class Connection {
|
|||||||
|
|
||||||
messages: for (const message of FeedMessage.deserialize(data)) {
|
messages: for (const message of FeedMessage.deserialize(data)) {
|
||||||
switch (message.action) {
|
switch (message.action) {
|
||||||
case Actions.BestBlock: {
|
case Actions.FeedVersion: {
|
||||||
const [best, blockTimestamp] = message.payload;
|
if (message.payload !== VERSION) {
|
||||||
|
this.state = this.update({ status: 'upgrade-requested' });
|
||||||
|
this.clean();
|
||||||
|
|
||||||
this.state = this.update({ best, blockTimestamp });
|
// Force reload from the server
|
||||||
|
setTimeout(() => window.location.reload(true), 3000);
|
||||||
|
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
continue messages;
|
||||||
|
}
|
||||||
|
|
||||||
|
case Actions.BestBlock: {
|
||||||
|
const [best, blockTimestamp, blockAverage] = message.payload;
|
||||||
|
|
||||||
|
this.state = this.update({ best, blockTimestamp, blockAverage });
|
||||||
|
|
||||||
continue messages;
|
continue messages;
|
||||||
}
|
}
|
||||||
@@ -215,6 +232,7 @@ export class Connection {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private handleDisconnect = async () => {
|
private handleDisconnect = async () => {
|
||||||
|
this.state = this.update({ status: 'offline' });
|
||||||
this.clean();
|
this.clean();
|
||||||
this.socket.close();
|
this.socket.close();
|
||||||
this.socket = await Connection.socket();
|
this.socket = await Connection.socket();
|
||||||
|
|||||||
@@ -25,11 +25,17 @@ function sortNodes(a: Node.Props, b: Node.Props): number {
|
|||||||
const aPropagation = a.blockDetails[4] == null ? Infinity : a.blockDetails[4] as number;
|
const aPropagation = a.blockDetails[4] == null ? Infinity : a.blockDetails[4] as number;
|
||||||
const bPropagation = b.blockDetails[4] == null ? Infinity : b.blockDetails[4] as number;
|
const bPropagation = b.blockDetails[4] == null ? Infinity : b.blockDetails[4] as number;
|
||||||
|
|
||||||
|
if (aPropagation === bPropagation) {
|
||||||
|
// Descending sort by block number
|
||||||
|
return b.blockDetails[0] - a.blockDetails[0];
|
||||||
|
}
|
||||||
|
|
||||||
|
// Ascending sort by propagation time
|
||||||
return aPropagation - bPropagation;
|
return aPropagation - bPropagation;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function Chain(props: Chain.Props) {
|
export function Chain(props: Chain.Props) {
|
||||||
const { best, blockTimestamp } = props.state;
|
const { best, blockTimestamp, blockAverage } = props.state;
|
||||||
|
|
||||||
const nodes = Array.from(props.state.nodes.values()).sort(sortNodes);
|
const nodes = Array.from(props.state.nodes.values()).sort(sortNodes);
|
||||||
|
|
||||||
@@ -37,6 +43,7 @@ export function Chain(props: Chain.Props) {
|
|||||||
<div className="Chain">
|
<div className="Chain">
|
||||||
<div className="Chain-header">
|
<div className="Chain-header">
|
||||||
<Tile icon={blockIcon} title="Best Block">#{formatNumber(best)}</Tile>
|
<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>
|
<Tile icon={lastTimeIcon} title="Last Block"><Ago when={blockTimestamp} /></Tile>
|
||||||
</div>
|
</div>
|
||||||
<div className="Chain-content">
|
<div className="Chain-content">
|
||||||
|
|||||||
@@ -23,7 +23,7 @@ export class Chains extends React.Component<Chains.Props, {}> {
|
|||||||
public render() {
|
public render() {
|
||||||
return (
|
return (
|
||||||
<div className="Chains">
|
<div className="Chains">
|
||||||
<Icon src={chainIcon} alt="Observed chain" />
|
<Icon src={chainIcon} alt="Observed Chain" />
|
||||||
{
|
{
|
||||||
this.chains.map((chain) => this.renderChain(chain))
|
this.chains.map((chain) => this.renderChain(chain))
|
||||||
}
|
}
|
||||||
@@ -38,10 +38,9 @@ export class Chains extends React.Component<Chains.Props, {}> {
|
|||||||
? 'Chains-chain Chains-chain-selected'
|
? 'Chains-chain Chains-chain-selected'
|
||||||
: 'Chains-chain';
|
: 'Chains-chain';
|
||||||
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<a key={label} className={className} onClick={this.subscribe.bind(this, label)}>
|
<a key={label} className={className} onClick={this.subscribe.bind(this, label)}>
|
||||||
{label} <span className="Chains-node-count">{nodeCount}</span>
|
{label} <span className="Chains-node-count" title="Node Count">{nodeCount}</span>
|
||||||
</a>
|
</a>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -20,14 +20,14 @@ export function Node(props: Node.Props) {
|
|||||||
return (
|
return (
|
||||||
<tr>
|
<tr>
|
||||||
<td>{name}</td>
|
<td>{name}</td>
|
||||||
<td>{implementation} v{version}</td>
|
<td style={{ width: 240 }}>{implementation} v{version}</td>
|
||||||
<td>{peers}</td>
|
<td style={{ width: 26 }}>{peers}</td>
|
||||||
<td>{txcount}</td>
|
<td style={{ width: 26 }}>{txcount}</td>
|
||||||
<td>#{formatNumber(height)}</td>
|
<td style={{ width: 88 }}>#{formatNumber(height)}</td>
|
||||||
<td><span title={hash}>{trimHash(hash, 16)}</span></td>
|
<td style={{ width: 154 }}><span title={hash}>{trimHash(hash, 16)}</span></td>
|
||||||
<td>{(blockTime / 1000).toFixed(3)}s</td>
|
<td style={{ width: 80 }}>{(blockTime / 1000).toFixed(3)}s</td>
|
||||||
<td>{propagationTime === null ? '∞' : `${propagationTime}ms`}</td>
|
<td style={{ width: 58 }}>{propagationTime === null ? '∞' : `${propagationTime}ms`}</td>
|
||||||
<td><Ago when={blockTimestamp} /></td>
|
<td style={{ width: 82 }}><Ago when={blockTimestamp} /></td>
|
||||||
</tr>
|
</tr>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,17 @@
|
|||||||
|
.OfflineIndicator {
|
||||||
|
position: absolute;
|
||||||
|
top: 30px;
|
||||||
|
right: 30px;
|
||||||
|
z-index: 10;
|
||||||
|
background: #c00;
|
||||||
|
line-height: 16px;
|
||||||
|
color: #fff;
|
||||||
|
font-size: 30px;
|
||||||
|
padding: 16px;
|
||||||
|
border-radius: 50px;
|
||||||
|
box-shadow: rgba(0,0,0,0.5) 0 3px 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.OfflineIndicator-upgrade {
|
||||||
|
background: #282;
|
||||||
|
}
|
||||||
@@ -0,0 +1,27 @@
|
|||||||
|
import * as React from 'react';
|
||||||
|
import './OfflineIndicator.css';
|
||||||
|
import { Icon } from './Icon';
|
||||||
|
import { State } from '../state';
|
||||||
|
import offlineIcon from '../icons/zap.svg';
|
||||||
|
import upgradeIcon from '../icons/flame.svg';
|
||||||
|
|
||||||
|
export namespace OfflineIndicator {
|
||||||
|
export interface Props {
|
||||||
|
status: State["status"];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export function OfflineIndicator(props: OfflineIndicator.Props): React.ReactElement<any> | null {
|
||||||
|
switch (props.status) {
|
||||||
|
case 'online':
|
||||||
|
return null;
|
||||||
|
case 'offline':
|
||||||
|
return <div className="OfflineIndicator"><Icon src={offlineIcon} alt="Offline" /></div>;
|
||||||
|
case 'upgrade-requested':
|
||||||
|
return (
|
||||||
|
<div className="OfflineIndicator OfflineIndicator-upgrade">
|
||||||
|
<Icon src={upgradeIcon} alt="New Version Available" />
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -4,3 +4,4 @@ export * from './Icon';
|
|||||||
export * from './Node';
|
export * from './Node';
|
||||||
export * from './Tile';
|
export * from './Tile';
|
||||||
export * from './Ago';
|
export * from './Ago';
|
||||||
|
export * from './OfflineIndicator';
|
||||||
|
|||||||
@@ -2,12 +2,14 @@ import { Node } from './components/Node';
|
|||||||
import { Types, Maybe } from '@dotstats/common';
|
import { Types, Maybe } from '@dotstats/common';
|
||||||
|
|
||||||
export interface State {
|
export interface State {
|
||||||
best: Types.BlockNumber,
|
status: 'online' | 'offline' | 'upgrade-requested';
|
||||||
blockTimestamp: Types.Timestamp,
|
best: Types.BlockNumber;
|
||||||
timeDiff: Types.Milliseconds,
|
blockTimestamp: Types.Timestamp;
|
||||||
subscribed: Maybe<Types.ChainLabel>,
|
blockAverage: Maybe<Types.Milliseconds>;
|
||||||
chains: Map<Types.ChainLabel, Types.NodeCount>,
|
timeDiff: Types.Milliseconds;
|
||||||
nodes: Map<Types.NodeId, Node.Props>,
|
subscribed: Maybe<Types.ChainLabel>;
|
||||||
|
chains: Map<Types.ChainLabel, Types.NodeCount>;
|
||||||
|
nodes: Map<Types.NodeId, Node.Props>;
|
||||||
}
|
}
|
||||||
|
|
||||||
export type Update = <K extends keyof State>(changes: Pick<State, K> | null) => Readonly<State>;
|
export type Update = <K extends keyof State>(changes: Pick<State, K> | null) => Readonly<State>;
|
||||||
|
|||||||
Reference in New Issue
Block a user