mirror of
https://github.com/pezkuwichain/pezkuwi-telemetry.git
synced 2026-06-12 14:41:15 +00:00
@@ -1,4 +1,5 @@
|
||||
import { Opaque, Maybe } from './helpers';
|
||||
import { stringify, parse, Stringified } from './stringify';
|
||||
import {
|
||||
FeedVersion,
|
||||
Address,
|
||||
@@ -123,11 +124,11 @@ export type Message =
|
||||
| Variants.PongMessage;
|
||||
|
||||
/**
|
||||
* Opaque data type to be sent to the feed. Passing through
|
||||
* strings means we can only serialize once, no matter how
|
||||
* many feed clients are listening in.
|
||||
* Data type to be sent to the feed. Passing through strings means we can only serialize once,
|
||||
* no matter how many feed clients are listening in.
|
||||
*/
|
||||
export type Data = Opaque<string, 'FeedMessage.Data'>;
|
||||
export interface SquashedMessages extends Array<Action | Payload> {};
|
||||
export type Data = Stringified<SquashedMessages>;
|
||||
|
||||
/**
|
||||
* Serialize an array of `Message`s to a single JSON string.
|
||||
@@ -137,7 +138,7 @@ export type Data = Opaque<string, 'FeedMessage.Data'>;
|
||||
* Action `string`s are converted to opcodes using the `actionToCode` mapping.
|
||||
*/
|
||||
export function serialize(messages: Array<Message>): Data {
|
||||
const squashed = new Array(messages.length * 2);
|
||||
const squashed: SquashedMessages = new Array(messages.length * 2);
|
||||
let index = 0;
|
||||
|
||||
messages.forEach((message) => {
|
||||
@@ -147,20 +148,20 @@ export function serialize(messages: Array<Message>): Data {
|
||||
squashed[index++] = payload;
|
||||
})
|
||||
|
||||
return JSON.stringify(squashed) as Data;
|
||||
return stringify(squashed);
|
||||
}
|
||||
|
||||
/**
|
||||
* Deserialize data to an array of `Message`s.
|
||||
*/
|
||||
export function deserialize(data: Data): Array<Message> {
|
||||
const json: Array<Action | Payload> = JSON.parse(data);
|
||||
const json = parse(data);
|
||||
|
||||
if (!Array.isArray(json) || json.length === 0 || json.length % 2 !== 0) {
|
||||
throw new Error('Invalid FeedMessage.Data');
|
||||
}
|
||||
|
||||
const messages: Array<Message> = new Array(json.length / 2);
|
||||
const messages = new Array<Message>(json.length / 2);
|
||||
|
||||
for (const index of messages.keys()) {
|
||||
const [ action, payload ] = json.slice(index * 2);
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
export * from './helpers';
|
||||
export * from './id';
|
||||
export * from './block';
|
||||
export * from './stringify';
|
||||
|
||||
import * as Types from './types';
|
||||
import * as FeedMessage from './feed';
|
||||
@@ -8,4 +9,4 @@ import * as FeedMessage from './feed';
|
||||
export { Types, FeedMessage };
|
||||
|
||||
// Increment this if breaking changes were made to types in `feed.ts`
|
||||
export const VERSION: Types.FeedVersion = 11 as Types.FeedVersion;
|
||||
export const VERSION: Types.FeedVersion = 12 as Types.FeedVersion;
|
||||
|
||||
@@ -0,0 +1,4 @@
|
||||
export abstract class Stringified<T> { public __PHANTOM__: T };
|
||||
|
||||
export const parse = JSON.parse as any as <T>(val: Stringified<T>) => T;
|
||||
export const stringify = JSON.stringify as any as <T>(val: T) => Stringified<T>;
|
||||
@@ -2,27 +2,52 @@ import * as React from 'react';
|
||||
import { Types } from '@dotstats/common';
|
||||
import { Chains, Chain, Ago, OfflineIndicator } from './components';
|
||||
import { Connection } from './Connection';
|
||||
import { Persistent } from './Persistent';
|
||||
import { State } from './state';
|
||||
|
||||
import './App.css';
|
||||
|
||||
export default class App extends React.Component<{}, State> {
|
||||
public state: State = {
|
||||
status: 'offline',
|
||||
best: 0 as Types.BlockNumber,
|
||||
blockTimestamp: 0 as Types.Timestamp,
|
||||
blockAverage: null,
|
||||
timeDiff: 0 as Types.Milliseconds,
|
||||
subscribed: null,
|
||||
chains: new Map(),
|
||||
nodes: new Map()
|
||||
};
|
||||
|
||||
public state: State;
|
||||
private connection: Promise<Connection>;
|
||||
private settings: Persistent<State.Settings>;
|
||||
private setSettings: Persistent<State.Settings>['set'];
|
||||
|
||||
constructor(props: {}) {
|
||||
super(props);
|
||||
|
||||
this.settings = new Persistent(
|
||||
'settings',
|
||||
{
|
||||
validator: true,
|
||||
implementation: true,
|
||||
peers: true,
|
||||
txs: true,
|
||||
cpu: true,
|
||||
mem: true,
|
||||
blocknumber: true,
|
||||
blockhash: true,
|
||||
blocktime: true,
|
||||
blockpropagation: true,
|
||||
blocklasttime: false
|
||||
},
|
||||
(settings) => this.setState({ settings })
|
||||
);
|
||||
|
||||
this.setSettings = this.settings.set.bind(this.settings);
|
||||
|
||||
this.state = {
|
||||
status: 'offline',
|
||||
best: 0 as Types.BlockNumber,
|
||||
blockTimestamp: 0 as Types.Timestamp,
|
||||
blockAverage: null,
|
||||
timeDiff: 0 as Types.Milliseconds,
|
||||
subscribed: null,
|
||||
chains: new Map(),
|
||||
nodes: new Map(),
|
||||
settings: this.settings.get(),
|
||||
};
|
||||
|
||||
this.connection = Connection.create((changes) => {
|
||||
if (changes) {
|
||||
this.setState(changes);
|
||||
@@ -50,7 +75,7 @@ export default class App extends React.Component<{}, State> {
|
||||
<div className="App">
|
||||
<OfflineIndicator status={status} />
|
||||
<Chains chains={chains} subscribed={subscribed} connection={this.connection} />
|
||||
<Chain appState={this.state} />
|
||||
<Chain appState={this.state} setSettings={this.setSettings} />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -0,0 +1,38 @@
|
||||
import { Maybe, Stringified, stringify, parse } from '@dotstats/common';
|
||||
|
||||
export class Persistent<Data> {
|
||||
private readonly onChange: (value: Data) => void;
|
||||
private readonly key: string;
|
||||
private value: Data;
|
||||
|
||||
constructor(key: string, initial: Data, onChange: (value: Data) => void) {
|
||||
this.key = key;
|
||||
this.onChange = onChange;
|
||||
|
||||
const stored = window.localStorage.getItem(key) as Maybe<Stringified<Data>>;
|
||||
|
||||
if (stored) {
|
||||
this.value = parse(stored);
|
||||
} else {
|
||||
this.value = initial;
|
||||
}
|
||||
|
||||
window.addEventListener('storage', (event) => {
|
||||
if (event.key === this.key) {
|
||||
this.value = parse(event.newValue as any as Stringified<Data>);
|
||||
|
||||
this.onChange(this.value);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
public get(): Data {
|
||||
return this.value;
|
||||
}
|
||||
|
||||
public set<K extends keyof Data>(changes: Pick<Data, K> | Data) {
|
||||
this.value = Object.assign({}, this.value, changes);
|
||||
window.localStorage.setItem(this.key, stringify(this.value) as any as string);
|
||||
this.onChange(this.value);
|
||||
}
|
||||
}
|
||||
+49
-5
@@ -8,11 +8,23 @@
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.Chain-map-toggle .Icon {
|
||||
|
||||
.Chain-tabs {
|
||||
position: absolute;
|
||||
right: 20px;
|
||||
right: 10px;
|
||||
bottom: 0;
|
||||
font-size: 32px;
|
||||
height: 40px;
|
||||
width: 200px;
|
||||
text-align: right;
|
||||
}
|
||||
|
||||
.Chain-tab-unit {
|
||||
display: inline-block;
|
||||
}
|
||||
|
||||
.Chain-tab-unit .Icon {
|
||||
margin-right: 10px;
|
||||
font-size: 26px;
|
||||
padding: 6px;
|
||||
background: #222;
|
||||
color: #aaa;
|
||||
@@ -21,7 +33,7 @@
|
||||
border-radius: 4px 4px 0 0;
|
||||
}
|
||||
|
||||
.Chain-map-toggle-on .Icon {
|
||||
.Chain-tab-unit-on .Icon {
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
@@ -44,7 +56,7 @@
|
||||
|
||||
.Chain-map {
|
||||
min-width: 1350px;
|
||||
background: url('../assets/world-map.svg') no-repeat;
|
||||
background: url('../../assets/world-map.svg') no-repeat;
|
||||
background-size: contain;
|
||||
background-position: center;
|
||||
position: absolute;
|
||||
@@ -71,3 +83,35 @@
|
||||
text-align: left;
|
||||
padding: 0.5em 1em;
|
||||
}
|
||||
|
||||
.Chain-settings {
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.Chain-settings-category {
|
||||
text-align: left;
|
||||
width: 500px;
|
||||
margin: 0 auto;
|
||||
padding: 2em 0;
|
||||
}
|
||||
|
||||
.Chain-settings-category h2 {
|
||||
padding: 0;
|
||||
margin: 0 0 0.5em 0;
|
||||
font-size: 20px;
|
||||
font-weight: 100;
|
||||
}
|
||||
|
||||
.Chain-settings-category p {
|
||||
padding: 0;
|
||||
margin: 0 0 0.1em 0;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.Chain-settings-category .Icon {
|
||||
margin-right: 10px;
|
||||
}
|
||||
|
||||
.Chain-settings-disabled {
|
||||
color: #666;
|
||||
}
|
||||
+85
-41
@@ -1,13 +1,17 @@
|
||||
import * as React from 'react';
|
||||
import { State as AppState } from '../state';
|
||||
import { formatNumber, secondsWithPrecision, viewport } from '../utils';
|
||||
import { Tile, Icon, Node, Ago } from './';
|
||||
import { State as AppState } from '../../state';
|
||||
import { formatNumber, secondsWithPrecision, viewport } from '../../utils';
|
||||
import { Tab } from './';
|
||||
import { Tile, Icon, Node, Ago } from '../';
|
||||
import { Types } from '@dotstats/common';
|
||||
import { Persistent } from '../../Persistent';
|
||||
|
||||
import blockIcon from '../icons/package.svg';
|
||||
import blockTimeIcon from '../icons/history.svg';
|
||||
import lastTimeIcon from '../icons/watch.svg';
|
||||
import worldIcon from '../icons/globe.svg';
|
||||
import blockIcon from '../../icons/package.svg';
|
||||
import blockTimeIcon from '../../icons/history.svg';
|
||||
import lastTimeIcon from '../../icons/watch.svg';
|
||||
import listIcon from '../../icons/list-unordered.svg';
|
||||
import worldIcon from '../../icons/globe.svg';
|
||||
import settingsIcon from '../../icons/settings.svg';
|
||||
|
||||
const MAP_RATIO = 800 / 350;
|
||||
const MAP_HEIGHT_ADJUST = 400 / 350;
|
||||
@@ -16,12 +20,15 @@ const HEADER = 148;
|
||||
import './Chain.css';
|
||||
|
||||
export namespace Chain {
|
||||
export type Display = 'list' | 'map' | 'settings';
|
||||
|
||||
export interface Props {
|
||||
appState: Readonly<AppState>;
|
||||
setSettings: Persistent<AppState.Settings>['set'];
|
||||
}
|
||||
|
||||
export interface State {
|
||||
display: 'map' | 'table';
|
||||
display: Display;
|
||||
map: {
|
||||
width: number;
|
||||
height: number;
|
||||
@@ -48,8 +55,19 @@ export class Chain extends React.Component<Chain.Props, Chain.State> {
|
||||
constructor(props: Chain.Props) {
|
||||
super(props);
|
||||
|
||||
let display: Chain.Display = 'list';
|
||||
|
||||
switch (window.location.hash) {
|
||||
case '#map':
|
||||
display = 'map';
|
||||
break;
|
||||
case '#settings':
|
||||
display = 'settings';
|
||||
break;
|
||||
}
|
||||
|
||||
this.state = {
|
||||
display: window.location.hash === '#map' ? 'map' : 'table',
|
||||
display,
|
||||
map: {
|
||||
width: 0,
|
||||
height: 0,
|
||||
@@ -71,13 +89,7 @@ export class Chain extends React.Component<Chain.Props, Chain.State> {
|
||||
|
||||
public render() {
|
||||
const { best, blockTimestamp, blockAverage } = this.props.appState;
|
||||
const { display } = this.state;
|
||||
|
||||
const toggleClass = ['Chain-map-toggle'];
|
||||
|
||||
if (display === 'map') {
|
||||
toggleClass.push('Chain-map-toggle-on');
|
||||
}
|
||||
const currentTab = this.state.display;
|
||||
|
||||
return (
|
||||
<div className="Chain">
|
||||
@@ -85,16 +97,20 @@ export class Chain extends React.Component<Chain.Props, Chain.State> {
|
||||
<Tile icon={blockIcon} title="Best Block">#{formatNumber(best)}</Tile>
|
||||
<Tile icon={blockTimeIcon} title="Average Time">{ blockAverage == null ? '-' : secondsWithPrecision(blockAverage / 1000) }</Tile>
|
||||
<Tile icon={lastTimeIcon} title="Last Block"><Ago when={blockTimestamp} /></Tile>
|
||||
<div className={toggleClass.join(' ')}>
|
||||
<Icon src={worldIcon} alt="Toggle Map" onClick={this.toggleMap} />
|
||||
<div className="Chain-tabs">
|
||||
<Tab icon={listIcon} label="List" display="list" hash="" current={currentTab} setDisplay={this.setDisplay} />
|
||||
<Tab icon={worldIcon} label="Map" display="map" hash="#map" current={currentTab} setDisplay={this.setDisplay} />
|
||||
<Tab icon={settingsIcon} label="Settings" display="settings" hash="#settings" current={currentTab} setDisplay={this.setDisplay} />
|
||||
</div>
|
||||
</div>
|
||||
<div className="Chain-content-container">
|
||||
<div className="Chain-content">
|
||||
{
|
||||
display === 'table'
|
||||
? this.renderTable()
|
||||
: this.renderMap()
|
||||
currentTab === 'list'
|
||||
? this.renderList()
|
||||
: currentTab === 'map'
|
||||
? this.renderMap()
|
||||
: this.renderSettings()
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
@@ -102,14 +118,26 @@ export class Chain extends React.Component<Chain.Props, Chain.State> {
|
||||
);
|
||||
}
|
||||
|
||||
private toggleMap = () => {
|
||||
if (this.state.display === 'map') {
|
||||
this.setState({ display: 'table' });
|
||||
window.location.hash = '';
|
||||
} else {
|
||||
this.setState({ display: 'map' });
|
||||
window.location.hash = '#map';
|
||||
}
|
||||
private setDisplay = (display: Chain.Display) => {
|
||||
this.setState({ display });
|
||||
};
|
||||
|
||||
private renderList() {
|
||||
const { settings } = this.props.appState;
|
||||
|
||||
return (
|
||||
<table className="Chain-node-list">
|
||||
<Node.Row.Header settings={settings} />
|
||||
<tbody>
|
||||
{
|
||||
this
|
||||
.nodes()
|
||||
.sort(sortNodes)
|
||||
.map((node) => <Node.Row key={node.id} node={node} settings={settings} />)
|
||||
}
|
||||
</tbody>
|
||||
</table>
|
||||
);
|
||||
}
|
||||
|
||||
private renderMap() {
|
||||
@@ -135,19 +163,35 @@ export class Chain extends React.Component<Chain.Props, Chain.State> {
|
||||
);
|
||||
}
|
||||
|
||||
private renderTable() {
|
||||
private renderSettings() {
|
||||
const { settings } = this.props.appState;
|
||||
|
||||
return (
|
||||
<table className="Chain-node-list">
|
||||
<Node.Row.Header />
|
||||
<tbody>
|
||||
{
|
||||
this
|
||||
.nodes()
|
||||
.sort(sortNodes)
|
||||
.map((node) => <Node.Row key={node.id} {...node} />)
|
||||
}
|
||||
</tbody>
|
||||
</table>
|
||||
<div className="Chain-settings">
|
||||
<div className="Chain-settings-category">
|
||||
<h2>Visible Columns</h2>
|
||||
{
|
||||
Node.Row.columns
|
||||
.map(({ label, icon, setting }, index) => {
|
||||
if (!setting) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const className = settings[setting] ? '' : 'Chain-settings-disabled';
|
||||
|
||||
const changeSetting = () => {
|
||||
const change = {};
|
||||
|
||||
change[setting] = !settings[setting];
|
||||
|
||||
this.props.setSettings(change);
|
||||
}
|
||||
|
||||
return <p key={index} className={className} onClick={changeSetting}><Icon src={icon} alt={label} /> {label}</p>;
|
||||
})
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -0,0 +1,34 @@
|
||||
import * as React from 'react';
|
||||
import { Chain } from './';
|
||||
import { Icon } from '../';
|
||||
|
||||
export namespace Tab {
|
||||
export interface Props {
|
||||
label: string;
|
||||
icon: string;
|
||||
display: Chain.Display;
|
||||
current: string;
|
||||
hash: string;
|
||||
setDisplay: (display: Chain.Display) => void;
|
||||
}
|
||||
}
|
||||
|
||||
export class Tab extends React.Component<Tab.Props, {}> {
|
||||
public render() {
|
||||
const { label, icon, display, current } = this.props;
|
||||
const highlight = display === current;
|
||||
const className = highlight ? 'Chain-tab-unit-on Chain-tab-unit' : 'Chain-tab-unit';
|
||||
|
||||
return (
|
||||
<div className={className} onClick={this.onClick}>
|
||||
<Icon src={icon} alt={label} />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
private onClick = () => {
|
||||
const { hash, display, setDisplay } = this.props;
|
||||
window.location.hash = hash;
|
||||
setDisplay(display);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,2 @@
|
||||
export * from './Chain';
|
||||
export * from './Tab';
|
||||
@@ -3,7 +3,7 @@ import Identicon from 'polkadot-identicon';
|
||||
import { formatNumber, trimHash, milliOrSecond, secondsWithPrecision } from '../../utils';
|
||||
import { State as AppState } from '../../state';
|
||||
import { SEMVER_PATTERN } from './';
|
||||
import { /*Ago,*/ Icon } from '../';
|
||||
import { Ago, Icon } from '../';
|
||||
|
||||
import nodeIcon from '../../icons/server.svg';
|
||||
import nodeValidatorIcon from '../../icons/shield.svg';
|
||||
@@ -14,41 +14,161 @@ import blockIcon from '../../icons/package.svg';
|
||||
import blockHashIcon from '../../icons/file-binary.svg';
|
||||
import blockTimeIcon from '../../icons/history.svg';
|
||||
import propagationTimeIcon from '../../icons/dashboard.svg';
|
||||
// import lastTimeIcon from '../../icons/watch.svg';
|
||||
import lastTimeIcon from '../../icons/watch.svg';
|
||||
import cpuIcon from '../../icons/microchip-solid.svg';
|
||||
import memoryIcon from '../../icons/memory-solid.svg';
|
||||
|
||||
import './Row.css';
|
||||
|
||||
export default class Row extends React.Component<AppState.Node, {}> {
|
||||
public static Header = () => {
|
||||
interface RowProps {
|
||||
node: AppState.Node;
|
||||
settings: AppState.Settings;
|
||||
};
|
||||
|
||||
interface HeaderProps {
|
||||
settings: AppState.Settings;
|
||||
};
|
||||
|
||||
interface Column {
|
||||
label: string;
|
||||
icon: string;
|
||||
width?: number;
|
||||
setting?: keyof AppState.Settings;
|
||||
render: (node: AppState.Node) => React.ReactElement<any> | string;
|
||||
}
|
||||
|
||||
export default class Row extends React.Component<RowProps, {}> {
|
||||
public static readonly columns: Column[] = [
|
||||
{
|
||||
label: 'Node',
|
||||
icon: nodeIcon,
|
||||
render: ({ nodeDetails }) => nodeDetails[0]
|
||||
},
|
||||
{
|
||||
label: 'Validator',
|
||||
icon: nodeValidatorIcon,
|
||||
width: 26,
|
||||
setting: 'validator',
|
||||
render: ({ nodeDetails }) => {
|
||||
const validator = nodeDetails[3];
|
||||
|
||||
return validator ? <span className="Node-Row-validator" title={validator}><Identicon id={validator} size={16} /></span> : '-';
|
||||
}
|
||||
},
|
||||
{
|
||||
label: 'Implementation',
|
||||
icon: nodeTypeIcon,
|
||||
width: 240,
|
||||
setting: 'implementation',
|
||||
render: ({ nodeDetails }) => {
|
||||
const [, implementation, version] = nodeDetails;
|
||||
const [semver] = version.match(SEMVER_PATTERN) || [version];
|
||||
|
||||
return <span title={`${implementation} v${version}`}>{implementation} v{semver}</span>;
|
||||
}
|
||||
},
|
||||
{
|
||||
label: 'Peer Count',
|
||||
icon: peersIcon,
|
||||
width: 26,
|
||||
setting: 'peers',
|
||||
render: ({ nodeStats }) => `${nodeStats[0]}`
|
||||
},
|
||||
{
|
||||
label: 'Transactions in Queue',
|
||||
icon: transactionsIcon,
|
||||
width: 26,
|
||||
setting: 'txs',
|
||||
render: ({ nodeStats }) => `${nodeStats[1]}`
|
||||
},
|
||||
{
|
||||
label: '% CPU Use',
|
||||
icon: cpuIcon,
|
||||
width: 26,
|
||||
setting: 'cpu',
|
||||
render: ({ nodeStats }) => {
|
||||
const cpu = nodeStats[3];
|
||||
|
||||
return cpu ? `${(cpu * 100).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> : '-';
|
||||
}
|
||||
},
|
||||
{
|
||||
label: 'Block',
|
||||
icon: blockIcon,
|
||||
width: 88,
|
||||
setting: 'blocknumber',
|
||||
render: ({ blockDetails }) => `#${formatNumber(blockDetails[0])}`
|
||||
},
|
||||
{
|
||||
label: 'Block Hash',
|
||||
icon: blockHashIcon,
|
||||
width: 154,
|
||||
setting: 'blockhash',
|
||||
render: ({ blockDetails }) => {
|
||||
const hash = blockDetails[1];
|
||||
|
||||
return <span title={hash}>{trimHash(hash, 16)}</span>;
|
||||
}
|
||||
},
|
||||
{
|
||||
label: 'Block Time',
|
||||
icon: blockTimeIcon,
|
||||
width: 80,
|
||||
setting: 'blocktime',
|
||||
render: ({ blockDetails }) => `${secondsWithPrecision(blockDetails[2]/1000)}`
|
||||
},
|
||||
{
|
||||
label: 'Block Propagation Time',
|
||||
icon: propagationTimeIcon,
|
||||
width: 58,
|
||||
setting: 'blockpropagation',
|
||||
render: ({ blockDetails }) => {
|
||||
const propagationTime = blockDetails[4];
|
||||
|
||||
return propagationTime === null ? '∞' : milliOrSecond(propagationTime as number);
|
||||
}
|
||||
},
|
||||
{
|
||||
label: 'Last Block Time',
|
||||
icon: lastTimeIcon,
|
||||
width: 100,
|
||||
setting: 'blocklasttime',
|
||||
render: ({ blockDetails }) => <Ago when={blockDetails[3]} />
|
||||
},
|
||||
];
|
||||
|
||||
public static Header = (props: HeaderProps) => {
|
||||
const { settings } = props;
|
||||
|
||||
return (
|
||||
<thead>
|
||||
<tr>
|
||||
<th><Icon src={nodeIcon} alt="Node" /></th>
|
||||
<th style={{ width: 26 }}><Icon src={nodeValidatorIcon} alt="Validator" /></th>
|
||||
<th style={{ width: 240 }}><Icon src={nodeTypeIcon} alt="Implementation" /></th>
|
||||
<th style={{ width: 26 }}><Icon src={peersIcon} alt="Peer Count" /></th>
|
||||
<th style={{ width: 26 }}><Icon src={transactionsIcon} alt="Transactions in Queue" /></th>
|
||||
<th style={{ width: 26 }}><Icon src={cpuIcon} alt="% CPU use" /></th>
|
||||
<th style={{ width: 26 }}><Icon src={memoryIcon} alt="Memory use" /></th>
|
||||
<th style={{ width: 88 }}><Icon src={blockIcon} alt="Block" /></th>
|
||||
<th style={{ width: 154 }}><Icon src={blockHashIcon} alt="Block Hash" /></th>
|
||||
<th style={{ width: 80 }}><Icon src={blockTimeIcon} alt="Block Time" /></th>
|
||||
<th style={{ width: 58 }}><Icon src={propagationTimeIcon} alt="Block Propagation Time" /></th>
|
||||
{/* <th style={{ width: 100 }}><Icon src={lastTimeIcon} alt="Last Block Time" /></th> */}
|
||||
{
|
||||
Row.columns
|
||||
.filter(({ setting }) => setting == null || settings[setting])
|
||||
.map(({ icon, width, label }, index) => (
|
||||
<th key={index} style={width ? { width } : undefined}><Icon src={icon} alt={label} /></th>
|
||||
))
|
||||
}
|
||||
</tr>
|
||||
</thead>
|
||||
)
|
||||
}
|
||||
|
||||
public render() {
|
||||
const { nodeDetails, blockDetails, nodeStats } = this.props;
|
||||
|
||||
const [name, implementation, version, validator] = nodeDetails;
|
||||
const [height, hash, blockTime, /*blockTimestamp*/, propagationTime] = blockDetails;
|
||||
const [peers, txcount, memory, cpu] = nodeStats;
|
||||
const [semver] = version.match(SEMVER_PATTERN) || [version];
|
||||
const { node, settings } = this.props;
|
||||
const propagationTime = node.blockDetails[4];
|
||||
|
||||
let className = 'Node-Row';
|
||||
|
||||
@@ -58,18 +178,11 @@ export default class Row extends React.Component<AppState.Node, {}> {
|
||||
|
||||
return (
|
||||
<tr className={className}>
|
||||
<td>{name}</td>
|
||||
<td>{validator ? <span className="Node-Row-validator" title={validator}><Identicon id={validator} size={16} /></span> : null}</td>
|
||||
<td><span title={`${implementation} v${version}`}>{implementation} v{semver}</span></td>
|
||||
<td>{peers}</td>
|
||||
<td>{txcount}</td>
|
||||
<td>{cpu ? `${(cpu * 100).toFixed(1)}%` : '-'}</td>
|
||||
<td>{memory ? <span title={`${memory}kb`}>{memory / 1024 | 0}mb</span> : '-'}</td>
|
||||
<td>#{formatNumber(height)}</td>
|
||||
<td><span title={hash}>{trimHash(hash, 16)}</span></td>
|
||||
<td>{secondsWithPrecision(blockTime/1000)}</td>
|
||||
<td>{propagationTime === null ? '∞' : milliOrSecond(propagationTime as number)}</td>
|
||||
{/* <td><Ago when={blockTimestamp} /></td> */}
|
||||
{
|
||||
Row.columns
|
||||
.filter(({ setting }) => setting == null || settings[setting])
|
||||
.map(({ render }, index) => <td key={index}>{render(node)}</td>)
|
||||
}
|
||||
</tr>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -8,6 +8,20 @@ export namespace State {
|
||||
blockDetails: Types.BlockDetails;
|
||||
location: Maybe<Types.NodeLocation>;
|
||||
}
|
||||
|
||||
export interface Settings {
|
||||
validator: boolean;
|
||||
implementation: boolean;
|
||||
peers: boolean;
|
||||
txs: boolean;
|
||||
cpu: boolean;
|
||||
mem: boolean;
|
||||
blocknumber: boolean;
|
||||
blockhash: boolean;
|
||||
blocktime: boolean;
|
||||
blockpropagation: boolean;
|
||||
blocklasttime: boolean;
|
||||
}
|
||||
}
|
||||
|
||||
export interface State {
|
||||
@@ -19,6 +33,7 @@ export interface State {
|
||||
subscribed: Maybe<Types.ChainLabel>;
|
||||
chains: Map<Types.ChainLabel, Types.NodeCount>;
|
||||
nodes: Map<Types.NodeId, State.Node>;
|
||||
settings: State.Settings;
|
||||
}
|
||||
|
||||
export type Update = <K extends keyof State>(changes: Pick<State, K> | null) => Readonly<State>;
|
||||
|
||||
Reference in New Issue
Block a user