Added ago timers

This commit is contained in:
maciejhirsz
2018-07-05 17:04:00 +02:00
parent 81ef3ee14e
commit 01da7dfc47
19 changed files with 200 additions and 48 deletions
+1 -1
View File
@@ -1,5 +1,5 @@
.App {
text-align: center;
text-align: left;
font-family: monospace, sans-serif;
}
+11 -5
View File
@@ -1,7 +1,6 @@
import * as React from 'react';
import { Types } from '@dotstats/common';
import { Node } from './Node';
import { Icon } from './Icon';
import { Node, Icon, Tile, Ago } from './components';
import { Connection } from './message';
import { State } from './state';
import { formatNumber } from './utils';
@@ -14,10 +13,13 @@ import transactionsIcon from './icons/inbox.svg';
import blockIcon from './icons/package.svg';
import blockHashIcon from './icons/file-binary.svg';
import blockTimeIcon from './icons/history.svg';
import lastTimeIcon from './icons/dashboard.svg';
export default class App extends React.Component<{}, State> {
public state: State = {
best: 0 as Types.BlockNumber,
blockTimestamp: 0 as Types.Timestamp,
timeDiff: 0 as Types.Milliseconds,
nodes: new Map()
};
@@ -28,11 +30,14 @@ export default class App extends React.Component<{}, State> {
}
public render() {
const { best, blockTimestamp, timeDiff } = this.state;
Ago.timeDiff = timeDiff;
return (
<div className="App">
<div className="App-header">
<Icon src={blockIcon} alt="Best Block" /> #{formatNumber(this.state.best)}
</div>
<Tile icon={blockIcon} title="Best Block">#{formatNumber(best)}</Tile>
<Tile icon={lastTimeIcon} title="Last Block"><Ago when={blockTimestamp} /></Tile>
<table className="App-list">
<thead>
<tr>
@@ -43,6 +48,7 @@ export default class App extends React.Component<{}, State> {
<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={lastTimeIcon} alt="Last Block Time" /></th>
</tr>
</thead>
<tbody>
+75
View File
@@ -0,0 +1,75 @@
import * as React from 'react';
import './Tile.css';
import { timestamp, Types } from '@dotstats/common';
export namespace Ago {
export interface Props {
when: Types.Timestamp,
}
export interface State {
now: Types.Timestamp,
}
}
const tickers = new Map<Ago, (ts: Types.Timestamp) => void>();
function tick() {
const now = timestamp();
for (const ticker of tickers.values()) {
ticker(now);
}
setTimeout(tick, 100);
}
tick();
export namespace Ago {
export interface State {
now: Types.Timestamp
}
}
export class Ago extends React.Component<Ago.Props, Ago.State> {
public static timeDiff = 0 as Types.Milliseconds;
public state: Ago.State;
constructor(props: Ago.Props) {
super(props);
this.state = {
now: (timestamp() + Ago.timeDiff) as Types.Timestamp
};
}
public componentWillMount() {
tickers.set(this, (now) => {
this.setState({
now: (now + Ago.timeDiff) as Types.Timestamp
});
})
}
public componentWillUnmount() {
tickers.delete(this);
}
public render() {
const ago = Math.max(this.state.now - this.props.when, 0) / 1000;
let agoStr: string;
if (ago < 10) {
agoStr = `${ago.toFixed(1)}s`;
} else if (ago < 60) {
agoStr = `${ago | 0}s`;
} else {
agoStr = `${ ago / 60 | 0}m`;
}
return <span title={new Date(this.props.when).toUTCString()}>{agoStr} ago</span>
}
}
@@ -1,5 +1,6 @@
import * as React from 'react';
import { formatNumber, trimHash } from './utils';
import { formatNumber, trimHash } from '../utils';
import { Ago } from './Ago';
import { Types } from '@dotstats/common';
export namespace Node {
@@ -13,7 +14,7 @@ export namespace Node {
export function Node(props: Node.Props) {
const [name, implementation, version] = props.nodeDetails;
const [height, hash, blockTime] = props.blockDetails;
const [height, hash, blockTime, blockTimestamp] = props.blockDetails;
const [peers, txcount] = props.nodeStats;
return (
@@ -24,7 +25,8 @@ export function Node(props: Node.Props) {
<td>{txcount}</td>
<td>#{formatNumber(height)}</td>
<td><span title={hash}>{trimHash(hash, 16)}</span></td>
<td>{blockTime / 1000}s</td>
<td>{(blockTime / 1000).toFixed(3)}s</td>
<td><Ago when={blockTimestamp} /></td>
</tr>
);
}
@@ -0,0 +1,7 @@
.Tile {
font-size: 2.5em;
padding: 20px;
text-align: left;
width: 7em;
display: inline-block;
}
+19
View File
@@ -0,0 +1,19 @@
import * as React from 'react';
import './Tile.css';
import { Icon } from './Icon';
export namespace Tile {
export interface Props {
title: string,
icon: string,
children?: React.ReactNode,
}
}
export function Tile(props: Tile.Props) {
return (
<div className="Tile">
<Icon src={props.icon} alt={props.title} /> {props.children}
</div>
);
}
@@ -0,0 +1,4 @@
export * from './Icon';
export * from './Node';
export * from './Tile';
export * from './Ago';
+19 -7
View File
@@ -1,4 +1,4 @@
import { FeedMessage, Types, Maybe, sleep } from '@dotstats/common';
import { timestamp, FeedMessage, Types, Maybe, sleep } from '@dotstats/common';
import { State, Update } from './state';
const { Actions } = FeedMessage;
@@ -13,7 +13,6 @@ export class Connection {
private static readonly address = `ws://${window.location.hostname}:8080`;
private static async socket(): Promise<WebSocket> {
let socket = await Connection.trySocket();
let timeout = TIMEOUT_BASE;
@@ -61,11 +60,11 @@ export class Connection {
constructor(socket: WebSocket, update: Update) {
this.socket = socket;
this.update = update;
this.state = update(null);
this.bindSocket();
}
private bindSocket() {
this.state = this.update({ nodes: new Map() });
this.socket.addEventListener('message', this.handleMessages);
this.socket.addEventListener('close', this.handleDisconnect);
this.socket.addEventListener('error', this.handleDisconnect);
@@ -85,10 +84,13 @@ export class Connection {
messages: for (const message of FeedMessage.deserialize(data)) {
switch (message.action) {
case Actions.BestBlock: {
this.state = this.update({ best: message.payload });
const [best, blockTimestamp] = message.payload;
this.state = this.update({ best, blockTimestamp });
continue messages;
}
case Actions.AddedNode: {
const [id, nodeDetails, nodeStats, blockDetails] = message.payload;
const node = { id, nodeDetails, nodeStats, blockDetails };
@@ -97,14 +99,15 @@ export class Connection {
break;
}
case Actions.RemovedNode: {
nodes.delete(message.payload);
break;
}
case Actions.ImportedBlock: {
const [id, blockDetails] = message.payload;
const node = nodes.get(id);
if (!node) {
@@ -115,9 +118,9 @@ export class Connection {
break;
}
case Actions.NodeStats: {
const [id, nodeStats] = message.payload;
const node = nodes.get(id);
if (!node) {
@@ -128,8 +131,17 @@ export class Connection {
break;
}
case Actions.TimeSync: {
this.state = this.update({
timeDiff: (timestamp() - message.payload) as Types.Milliseconds
});
continue messages;
}
default: {
return;
continue messages;
}
}
}
+3 -1
View File
@@ -1,8 +1,10 @@
import { Types } from '@dotstats/common';
import { Node } from './Node';
import { Node } from './components/Node';
export interface State {
best: Types.BlockNumber,
blockTimestamp: Types.Timestamp,
timeDiff: Types.Milliseconds,
nodes: Map<Types.NodeId, Node.Props>
}