Turbo Render (#298)

* More responsive React updates on scroll

* `Icon`s now use shadow dom

* Faster Sparkline

* Recycle table rows

* Separate Header from Chain to avoid vdom diffing

* Separate THead from Row.HEADER to avoid vdom diffing

* Throttle rendering updates on chain tabs, also styles

* Minor tweaks and fixes

* Created components for all columns

* Wrapping up Column refactor

* Rename Row--td to Column

* Lazy `Ago`

* Update styles for faster layouting

* Minor cleanup

* Fix Connection

* Use shadow DOM in `PolkadotIcon`

* Comments and tweaks for the List component

* Faster Tooltip and Truncate

* Minor tweaks

* Tooltiped columns can now be copied

* Future-proof Connection

* Remove the <div> wrapper from Icon

* Fix dash on missing graph data

* Clean up some SVGs

* Cleanup and comments

* Localize the use of `previousKeys` to `recalculateKeys`

* Custom appState disjoint from React component state

* Make appState and appUpdate refs readonly

* Cleanup
This commit is contained in:
Maciej Hirsz
2020-11-11 13:41:01 +01:00
committed by GitHub
parent 675776c3e1
commit ebb01c1a7d
72 changed files with 1863 additions and 1118 deletions
+2 -5
View File
@@ -16,17 +16,15 @@
"clean": "rm -rf node_modules build .nyc env-config.js report*.json yarn-error.log"
},
"dependencies": {
"@fnando/sparkline": "maciejhirsz/sparkline",
"@polkadot/util-crypto": "^2.8.1",
"@types/react-measure": "^2.0.6",
"blakejs": "^1.1.0",
"husky": "^4.2.5",
"lint-staged": "^10.1.7",
"react": "^16.13.1",
"react-dom": "^16.13.1",
"react": "^17.0.1",
"react-dom": "^17.0.1",
"react-measure": "^2.3.0",
"react-scripts-ts": "3.1.0",
"react-svg": "4.1.1",
"stable": "^0.1.8",
"tslint": "^6.1.1"
},
@@ -34,7 +32,6 @@
"@types/node": "^13.13.2",
"@types/react": "^16.9.34",
"@types/react-dom": "^16.9.6",
"@types/react-svg": "3.0.0",
"@types/tape": "^4.13.0",
"babel-preset-env": "^1.7.0",
"babel-preset-react": "^6.24.1",
+18 -18
View File
@@ -1,17 +1,17 @@
import { Types } from './common';
import { State, UpdateBound } from './state';
import { State, Update } from './state';
import { ConsensusDetail } from './common/types';
// Number of blocks which are kept in memory
const BLOCKS_LIMIT = 50;
export class AfgHandling {
private updateState: UpdateBound;
private getState: () => Readonly<State>;
constructor(updateState: UpdateBound, getState: () => Readonly<State>) {
this.updateState = updateState;
this.getState = getState;
constructor(
private readonly appUpdate: Update,
private readonly appState: Readonly<State>
) {
this.appUpdate = appUpdate;
this.appState = appState;
}
public receivedAuthoritySet(
@@ -19,19 +19,19 @@ export class AfgHandling {
authorities: Types.Authorities
) {
if (
this.getState().authoritySetId != null &&
authoritySetId !== this.getState().authoritySetId
this.appState.authoritySetId != null &&
authoritySetId !== this.appState.authoritySetId
) {
// the visualization is restarted when we receive a new authority set
this.updateState({
this.appUpdate({
authoritySetId,
authorities,
consensusInfo: [],
displayConsensusLoadingScreen: false,
});
} else if (this.getState().authoritySetId == null) {
} else if (this.appState.authoritySetId == null) {
// initial display
this.updateState({
this.appUpdate({
authoritySetId,
authorities,
consensusInfo: [],
@@ -46,7 +46,7 @@ export class AfgHandling {
finalizedNumber: Types.BlockNumber,
finalizedHash: Types.BlockHash
) {
const state = this.getState();
const state = this.appState;
if (finalizedNumber < state.best - BLOCKS_LIMIT) {
return;
}
@@ -108,7 +108,7 @@ export class AfgHandling {
this.backfill(state.consensusInfo, finalizedNumber, op, addr, addr);
this.pruneBlocks(state.consensusInfo);
this.updateState({ consensusInfo: state.consensusInfo });
this.appUpdate({ consensusInfo: state.consensusInfo });
}
public receivedPre(
@@ -117,7 +117,7 @@ export class AfgHandling {
voter: Types.Address,
what: string
) {
const state = this.getState();
const state = this.appState;
if (height < state.best - BLOCKS_LIMIT) {
return;
}
@@ -165,11 +165,11 @@ export class AfgHandling {
consensusInfo[index][1][addr][voter].ImplicitPointer = height;
return true;
};
const consensusInfo = this.getState().consensusInfo;
const consensusInfo = this.appState.consensusInfo;
this.backfill(consensusInfo, height, op, addr, voter);
this.pruneBlocks(consensusInfo);
this.updateState({ consensusInfo });
this.appUpdate({ consensusInfo });
}
// Initializes the `ConsensusView` with empty objects.
@@ -239,7 +239,7 @@ export class AfgHandling {
}
let firstBlockNumber = consensusInfo[consensusInfo.length - 1][0];
const limit = this.getState().best - BLOCKS_LIMIT;
const limit = this.appState.best - BLOCKS_LIMIT;
if (firstBlockNumber < limit) {
firstBlockNumber = limit as Types.BlockNumber;
}
+1
View File
@@ -2,6 +2,7 @@
text-align: left;
font-family: Roboto, Helvetica, Arial, sans-serif;
font-size: 16px;
min-width: 1318px;
}
.App-no-telemetry {
+37 -27
View File
@@ -4,15 +4,24 @@ import { AllChains, Chains, Chain, Ago, OfflineIndicator } from './components';
import { Row, Column } from './components/List';
import { Connection } from './Connection';
import { Persistent, PersistentObject, PersistentSet } from './persist';
import { State, Node, ChainData, comparePinnedChains } from './state';
import {
bindState,
State,
Update,
Node,
ChainData,
comparePinnedChains,
} from './state';
import { getHashData } from './utils';
import stable from 'stable';
import './App.css';
export default class App extends React.Component<{}, State> {
public state: State;
export default class App extends React.Component<{}, {}> {
private chainsCache: ChainData[] = [];
// Custom state for finer control over updates
private readonly appState: Readonly<State>;
private readonly appUpdate: Update;
private readonly settings: PersistentObject<State.Settings>;
private readonly pins: PersistentSet<Types.NodeName>;
private readonly sortBy: Persistent<Maybe<number>>;
@@ -52,28 +61,28 @@ export default class App extends React.Component<{}, State> {
const selectedColumns = this.selectedColumns(settings);
this.sortBy.set(null);
this.setState({ settings, selectedColumns, sortBy: null });
this.appUpdate({ settings, selectedColumns, sortBy: null });
}
);
this.pins = new PersistentSet<Types.NodeName>('pinned_names', (pins) => {
const { nodes } = this.state;
const { nodes } = this.appState;
nodes.mutEachAndSort((node) => node.setPinned(pins.has(node.name)));
this.setState({ nodes, pins });
this.appUpdate({ nodes, pins });
});
this.sortBy = new Persistent<Maybe<number>>('sortBy', null, (sortBy) => {
const compare = this.getComparator(sortBy);
this.state.nodes.setComparator(compare);
this.setState({ sortBy });
this.appState.nodes.setComparator(compare);
this.appUpdate({ sortBy });
});
const { tab = '' } = getHashData();
this.state = {
this.appUpdate = bindState(this, {
status: 'offline',
best: 0 as Types.BlockNumber,
finalized: 0 as Types.BlockNumber,
@@ -93,23 +102,23 @@ export default class App extends React.Component<{}, State> {
sortBy: this.sortBy.get(),
selectedColumns: this.selectedColumns(this.settings.raw()),
tab,
};
this.state.nodes.setComparator(this.getComparator(this.sortBy.get()));
this.connection = Connection.create(this.pins, (changes) => {
if (changes) {
this.setState(changes);
}
return this.state;
});
this.appState = this.appUpdate({});
const comparator = this.getComparator(this.sortBy.get());
this.appState.nodes.setComparator(comparator);
this.connection = Connection.create(
this.pins,
this.appState,
this.appUpdate
);
setInterval(() => (this.chainsCache = []), 10000); // Wipe sorted chains cache every 10 seconds
}
public render() {
const { timeDiff, subscribed, status, tab } = this.state;
const { timeDiff, subscribed, status, tab } = this.appState;
const chains = this.chains();
Ago.timeDiff = timeDiff;
@@ -141,7 +150,8 @@ export default class App extends React.Component<{}, State> {
connection={this.connection}
/>
<Chain
appState={this.state}
appState={this.appState}
appUpdate={this.appUpdate}
connection={this.connection}
settings={this.settings}
pins={this.pins}
@@ -152,7 +162,7 @@ export default class App extends React.Component<{}, State> {
);
}
public componentWillMount() {
public componentDidMount() {
window.addEventListener('keydown', this.onKeyPress);
window.addEventListener('hashchange', this.onHashChange);
}
@@ -170,8 +180,8 @@ export default class App extends React.Component<{}, State> {
event.preventDefault();
const { subscribed } = this.state;
const chains = Array.from(this.state.chains.keys());
const { subscribed } = this.appState;
const chains = Array.from(this.appState.chains.keys());
let index = 0;
@@ -196,12 +206,12 @@ export default class App extends React.Component<{}, State> {
};
private chains(): ChainData[] {
if (this.chainsCache.length === this.state.chains.size) {
if (this.chainsCache.length === this.appState.chains.size) {
return this.chainsCache;
}
this.chainsCache = stable.inplace(
Array.from(this.state.chains.values()),
Array.from(this.appState.chains.values()),
(a, b) => {
const pinned = comparePinnedChains(a.label, b.label);
@@ -223,7 +233,7 @@ export default class App extends React.Component<{}, State> {
}
private getComparator(sortBy: Maybe<number>): Compare<Node> {
const columns = this.state.selectedColumns;
const columns = this.appState.selectedColumns;
if (sortBy != null) {
const [index, rev] = sortBy < 0 ? [~sortBy, -1] : [sortBy, 1];
+78 -77
View File
@@ -1,18 +1,21 @@
import { VERSION, timestamp, FeedMessage, Types, Maybe, sleep } from './common';
import {
State,
Update,
Node,
UpdateBound,
ChainData,
PINNED_CHAINS,
} from './state';
import { State, Update, Node, ChainData, PINNED_CHAINS } from './state';
import { PersistentSet } from './persist';
import { getHashData, setHashData } from './utils';
import { AfgHandling } from './AfgHandling';
import { VIS_AUTHORITIES_LIMIT } from './components/Consensus';
import { Column } from './components/List';
import { ACTIONS } from './common/feed';
import {
Column,
LocationColumn,
PeersColumn,
TxsColumn,
FinalizedBlockColumn,
FinalizedHashColumn,
UploadColumn,
DownloadColumn,
StateCacheColumn,
} from './components/List';
const TIMEOUT_BASE = (1000 * 5) as Types.Milliseconds; // 5 seconds
const TIMEOUT_MAX = (1000 * 60 * 5) as Types.Milliseconds; // 5 minutes
@@ -26,9 +29,10 @@ declare global {
export class Connection {
public static async create(
pins: PersistentSet<Types.NodeName>,
update: Update
appState: Readonly<State>,
appUpdate: Update
): Promise<Connection> {
return new Connection(await Connection.socket(), update, pins);
return new Connection(await Connection.socket(), appState, appUpdate, pins);
}
private static readonly utf8decoder = new TextDecoder('utf-8');
@@ -102,27 +106,22 @@ export class Connection {
private resubscribeTo: Maybe<Types.ChainLabel> = getHashData().chain;
// flag whether or not FE should subscribe to consensus updates on reconnect
private resubscribeSendFinality: boolean = getHashData().tab === 'consensus';
// flag used to throttle DOM updates to window frame rate
private isUpdating = false;
private socket: WebSocket;
private state: Readonly<State>;
private readonly update: Update;
private readonly pins: PersistentSet<Types.NodeName>;
constructor(
socket: WebSocket,
update: Update,
pins: PersistentSet<Types.NodeName>
private socket: WebSocket,
private readonly appState: Readonly<State>,
private readonly appUpdate: Update,
private readonly pins: PersistentSet<Types.NodeName>
) {
this.socket = socket;
this.update = update;
this.pins = pins;
this.bindSocket();
}
public subscribe(chain: Types.ChainLabel) {
if (this.state.subscribed != null && this.state.subscribed !== chain) {
this.state = this.update({
if (
this.appState.subscribed != null &&
this.appState.subscribed !== chain
) {
this.appUpdate({
tab: 'list',
});
setHashData({ chain, tab: 'list' });
@@ -134,7 +133,7 @@ export class Connection {
}
public subscribeConsensus(chain: Types.ChainLabel) {
if (this.state.authorities.length <= VIS_AUTHORITIES_LIMIT) {
if (this.appState.authorities.length <= VIS_AUTHORITIES_LIMIT) {
setHashData({ chain });
this.resubscribeSendFinality = true;
this.socket.send(`send-finality:${chain}`);
@@ -142,7 +141,7 @@ export class Connection {
}
public resetConsensus() {
this.state = this.update({
this.appUpdate({
consensusInfo: new Array() as Types.ConsensusInfo,
displayConsensusLoadingScreen: true,
authorities: [] as Types.Address[],
@@ -156,14 +155,10 @@ export class Connection {
}
public handleMessages = (messages: FeedMessage.Message[]) => {
const { nodes, chains, sortBy, selectedColumns } = this.state;
const { nodes, chains, sortBy, selectedColumns } = this.appState;
const nodesStateRef = nodes.ref;
const updateState: UpdateBound = (state) => {
this.state = this.update(state);
};
const getState = () => this.state;
const afg = new AfgHandling(updateState, getState);
const afg = new AfgHandling(this.appUpdate, this.appState);
let sortByColumn: Maybe<Column> = null;
@@ -176,13 +171,7 @@ export class Connection {
switch (message.action) {
case ACTIONS.FeedVersion: {
if (message.payload !== VERSION) {
this.state = this.update({ status: 'upgrade-requested' });
this.clean();
// Force reload from the server
setTimeout(() => window.location.reload(true), 3000);
return;
return this.newVersion();
}
break;
@@ -193,7 +182,7 @@ export class Connection {
nodes.mutEach((node) => node.newBestBlock());
this.state = this.update({ best, blockTimestamp, blockAverage });
this.appUpdate({ best, blockTimestamp, blockAverage });
break;
}
@@ -201,7 +190,7 @@ export class Connection {
case ACTIONS.BestFinalized: {
const [finalized /*, hash */] = message.payload;
this.state = this.update({ finalized });
this.appUpdate({ finalized });
break;
}
@@ -257,7 +246,7 @@ export class Connection {
nodes.mutAndMaybeSort(
id,
(node) => node.updateLocation([lat, lon, city]),
sortByColumn === Column.LOCATION
sortByColumn === LocationColumn
);
break;
@@ -277,8 +266,8 @@ export class Connection {
nodes.mutAndMaybeSort(
id,
(node) => node.updateFinalized(height, hash),
sortByColumn === Column.FINALIZED ||
sortByColumn === Column.FINALIZED_HASH
sortByColumn === FinalizedBlockColumn ||
sortByColumn === FinalizedHashColumn
);
break;
@@ -290,7 +279,7 @@ export class Connection {
nodes.mutAndMaybeSort(
id,
(node) => node.updateStats(nodeStats),
sortByColumn === Column.PEERS || sortByColumn === Column.TXS
sortByColumn === PeersColumn || sortByColumn === TxsColumn
);
break;
@@ -302,7 +291,7 @@ export class Connection {
nodes.mutAndMaybeSort(
id,
(node) => node.updateHardware(nodeHardware),
sortByColumn === Column.UPLOAD || sortByColumn === Column.DOWNLOAD
sortByColumn === UploadColumn || sortByColumn === DownloadColumn
);
break;
@@ -314,14 +303,14 @@ export class Connection {
nodes.mutAndMaybeSort(
id,
(node) => node.updateIO(nodeIO),
sortByColumn === Column.STATE_CACHE
sortByColumn === StateCacheColumn
);
break;
}
case ACTIONS.TimeSync: {
this.state = this.update({
this.appUpdate({
timeDiff: (timestamp() - message.payload) as Types.Milliseconds,
});
@@ -338,7 +327,7 @@ export class Connection {
chains.set(label, { label, nodeCount });
}
this.state = this.update({ chains });
this.appUpdate({ chains });
break;
}
@@ -346,9 +335,9 @@ export class Connection {
case ACTIONS.RemovedChain: {
chains.delete(message.payload);
if (this.state.subscribed === message.payload) {
if (this.appState.subscribed === message.payload) {
nodes.clear();
this.state = this.update({ subscribed: null, nodes, chains });
this.appUpdate({ subscribed: null, nodes, chains });
this.resetConsensus();
}
@@ -358,16 +347,16 @@ export class Connection {
case ACTIONS.SubscribedTo: {
nodes.clear();
this.state = this.update({ subscribed: message.payload, nodes });
this.appUpdate({ subscribed: message.payload, nodes });
break;
}
case ACTIONS.UnsubscribedFrom: {
if (this.state.subscribed === message.payload) {
if (this.appState.subscribed === message.payload) {
nodes.clear();
this.state = this.update({ subscribed: null, nodes });
this.appUpdate({ subscribed: null, nodes });
}
break;
@@ -416,12 +405,8 @@ export class Connection {
}
}
if (nodes.hasChangedSince(nodesStateRef) && !this.isUpdating) {
this.isUpdating = true;
window.requestAnimationFrame(() => {
this.update({ nodes });
this.isUpdating = false;
});
if (nodes.hasChangedSince(nodesStateRef)) {
this.appUpdate({ nodes });
}
this.autoSubscribe();
@@ -430,19 +415,19 @@ export class Connection {
private bindSocket() {
this.ping();
if (this.state) {
const { nodes } = this.state;
if (this.appState) {
const { nodes } = this.appState;
nodes.clear();
}
this.state = this.update({
this.appUpdate({
status: 'online',
});
if (this.state.subscribed) {
this.resubscribeTo = this.state.subscribed;
this.resubscribeSendFinality = this.state.sendFinality;
this.state = this.update({ subscribed: null, sendFinality: false });
if (this.appState.subscribed) {
this.resubscribeTo = this.appState.subscribed;
this.resubscribeSendFinality = this.appState.sendFinality;
this.appUpdate({ subscribed: null, sendFinality: false });
}
this.socket.addEventListener('message', this.handleFeedData);
@@ -479,8 +464,14 @@ export class Connection {
const latency = timestamp() - this.pingSent;
this.pingSent = null;
}
console.log('latency', latency);
private newVersion() {
this.appUpdate({ status: 'upgrade-requested' });
this.clean();
// Force reload from the server
setTimeout(() => window.location.reload(true), 3000);
}
private clean() {
@@ -493,18 +484,28 @@ export class Connection {
}
private handleFeedData = (event: MessageEvent) => {
const data =
typeof event.data === 'string'
? ((event.data as any) as FeedMessage.Data)
: ((Connection.utf8decoder.decode(
event.data
) as any) as FeedMessage.Data);
let data: FeedMessage.Data;
if (typeof event.data === 'string') {
data = (event.data as any) as FeedMessage.Data;
} else {
const u8aData = new Uint8Array(event.data);
// Future-proofing for when we switch to binary feed
if (u8aData[0] === 0x00) {
return this.newVersion();
}
const str = Connection.utf8decoder.decode(event.data);
data = (str as any) as FeedMessage.Data;
}
this.handleMessages(FeedMessage.deserialize(data));
};
private autoSubscribe() {
const { subscribed, chains } = this.state;
const { subscribed, chains } = this.appState;
const { resubscribeTo, resubscribeSendFinality } = this;
if (subscribed) {
@@ -540,7 +541,7 @@ export class Connection {
}
private handleDisconnect = async () => {
this.state = this.update({ status: 'offline' });
this.appUpdate({ status: 'offline' });
this.resetConsensus();
this.clean();
this.socket.close();
+6
View File
@@ -181,7 +181,13 @@ export class SortedCollection<Item extends { id: number }> {
return;
}
const index = sortedIndexOf(item, this.list, this.compare);
mutator(item);
if (index >= this.focus.start && index < this.focus.end) {
this.changeRef += 1;
}
}
public mutAndSort(id: number, mutator: (item: Item) => void) {
+23 -5
View File
@@ -38,15 +38,29 @@ export class Ago extends React.Component<Ago.Props, Ago.State> {
public state: Ago.State;
private agoStr: string;
constructor(props: Ago.Props) {
super(props);
this.state = {
now: (timestamp() - Ago.timeDiff) as Types.Timestamp,
};
this.agoStr = this.stringify(props.when, this.state.now);
}
public componentWillMount() {
public shouldComponentUpdate(nextProps: Ago.Props, nextState: Ago.State) {
const nextAgoStr = this.stringify(nextProps.when, nextState.now);
if (this.agoStr !== nextAgoStr) {
this.agoStr = nextAgoStr;
return true;
}
return false;
}
public componentDidMount() {
tickers.set(this, (now) => {
this.setState({
now: (now - Ago.timeDiff) as Types.Timestamp,
@@ -63,7 +77,13 @@ export class Ago extends React.Component<Ago.Props, Ago.State> {
return <span>-</span>;
}
const ago = Math.max(this.state.now - this.props.when, 0) / 1000;
return (
<span title={new Date(this.props.when).toUTCString()}>{this.agoStr}</span>
);
}
private stringify(when: number, now: number): string {
const ago = Math.max(now - when, 0) / 1000;
let agoStr: string;
@@ -83,8 +103,6 @@ export class Ago extends React.Component<Ago.Props, Ago.State> {
agoStr += ' ago';
}
return (
<span title={new Date(this.props.when).toUTCString()}>{agoStr}</span>
);
return agoStr;
}
}
+2 -2
View File
@@ -19,12 +19,12 @@ export class AllChains extends React.Component<AllChains.Props, {}> {
const close = subscribed ? `#list/${subscribed}` : '#list';
return (
<React.Fragment>
<>
<a className="AllChains-overlay" href={close} />
<div className="AllChains">
{chains.map((chain) => this.renderChain(chain))}
</div>
</React.Fragment>
</>
);
}
-18
View File
@@ -1,21 +1,3 @@
.Chain-header {
width: 100%;
height: 108px;
overflow: hidden;
background: #fff;
color: #000;
min-width: 1350px;
position: relative;
}
.Chain-tabs {
position: absolute;
right: 5px;
bottom: 10px;
width: 200px;
text-align: right;
}
.Chain-content-container {
position: absolute;
left: 0;
+19 -64
View File
@@ -1,21 +1,12 @@
import * as React from 'react';
import { Connection } from '../../Connection';
import { Types, Maybe } from '../../common';
import { State as AppState } from '../../state';
import { formatNumber, secondsWithPrecision, getHashData } from '../../utils';
import { Tab } from './';
import { State as AppState, Update as AppUpdate } from '../../state';
import { getHashData } from '../../utils';
import { Header } from './';
import { Tile, Ago, List, Map, Settings, Consensus } from '../';
import { Persistent, PersistentObject, PersistentSet } from '../../persist';
import blockIcon from '../../icons/cube.svg';
import finalizedIcon from '../../icons/cube-alt.svg';
import blockTimeIcon from '../../icons/history.svg';
import lastTimeIcon from '../../icons/watch.svg';
import listIcon from '../../icons/list-alt-regular.svg';
import worldIcon from '../../icons/location.svg';
import settingsIcon from '../../icons/settings.svg';
import consensusIcon from '../../icons/cube-alt.svg';
import './Chain.css';
export namespace Chain {
@@ -23,6 +14,7 @@ export namespace Chain {
export interface Props {
appState: Readonly<AppState>;
appUpdate: AppUpdate;
connection: Promise<Connection>;
settings: PersistentObject<AppState.Settings>;
pins: PersistentSet<Types.NodeName>;
@@ -64,56 +56,14 @@ export class Chain extends React.Component<Chain.Props, Chain.State> {
return (
<div className="Chain">
<div className="Chain-header">
<Tile icon={blockIcon} title="Best Block">
#{formatNumber(best)}
</Tile>
<Tile icon={finalizedIcon} title="Finalized Block">
#{formatNumber(finalized)}
</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="Chain-tabs">
<Tab
icon={listIcon}
label="List"
display="list"
tab=""
current={currentTab}
setDisplay={this.setDisplay}
/>
<Tab
icon={worldIcon}
label="Map"
display="map"
tab="map"
current={currentTab}
setDisplay={this.setDisplay}
/>
<Tab
icon={consensusIcon}
label="Consensus"
display="consensus"
tab="consensus"
current={currentTab}
setDisplay={this.setDisplay}
/>
<Tab
icon={settingsIcon}
label="Settings"
display="settings"
tab="settings"
current={currentTab}
setDisplay={this.setDisplay}
/>
</div>
</div>
<Header
best={best}
finalized={finalized}
blockAverage={blockAverage}
blockTimestamp={blockTimestamp}
currentTab={currentTab}
setDisplay={this.setDisplay}
/>
<div className="Chain-content-container">
<div className="Chain-content">{this.renderContent()}</div>
</div>
@@ -128,14 +78,19 @@ export class Chain extends React.Component<Chain.Props, Chain.State> {
return <Settings settings={this.props.settings} />;
}
const { appState, connection, pins, sortBy } = this.props;
const { appState, appUpdate, connection, pins, sortBy } = this.props;
if (display === 'consensus') {
return <Consensus appState={appState} connection={connection} />;
}
return display === 'list' ? (
<List appState={appState} pins={pins} sortBy={sortBy} />
<List
appState={appState}
appUpdate={appUpdate}
pins={pins}
sortBy={sortBy}
/>
) : (
<Map appState={appState} />
);
+17
View File
@@ -0,0 +1,17 @@
.Header {
width: 100%;
height: 108px;
overflow: hidden;
background: #fff;
color: #000;
min-width: 1350px;
position: relative;
}
.Header-tabs {
position: absolute;
right: 5px;
bottom: 10px;
width: 200px;
text-align: right;
}
+97
View File
@@ -0,0 +1,97 @@
import * as React from 'react';
import { Types, Maybe } from '../../common';
import { formatNumber, secondsWithPrecision } from '../../utils';
import { Tab, Chain } from './';
import { Tile, Ago } from '../';
import blockIcon from '../../icons/cube.svg';
import finalizedIcon from '../../icons/cube-alt.svg';
import blockTimeIcon from '../../icons/history.svg';
import lastTimeIcon from '../../icons/watch.svg';
import listIcon from '../../icons/list-alt-regular.svg';
import worldIcon from '../../icons/location.svg';
import settingsIcon from '../../icons/settings.svg';
import consensusIcon from '../../icons/cube-alt.svg';
import './Header.css';
export namespace Header {
export interface Props {
best: Types.BlockNumber;
finalized: Types.BlockNumber;
blockTimestamp: Types.Timestamp;
blockAverage: Maybe<Types.Milliseconds>;
currentTab: Chain.Display;
setDisplay: (display: Chain.Display) => void;
}
}
export class Header extends React.Component<Header.Props, {}> {
public shouldComponentUpdate(nextProps: Header.Props) {
return (
this.props.best !== nextProps.best ||
this.props.finalized !== nextProps.finalized ||
this.props.blockTimestamp !== nextProps.blockTimestamp ||
this.props.blockAverage !== nextProps.blockAverage ||
this.props.currentTab !== nextProps.currentTab
);
}
public render() {
const { best, finalized, blockTimestamp, blockAverage } = this.props;
const { currentTab, setDisplay } = this.props;
return (
<div className="Header">
<Tile icon={blockIcon} title="Best Block">
#{formatNumber(best)}
</Tile>
<Tile icon={finalizedIcon} title="Finalized Block">
#{formatNumber(finalized)}
</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="Header-tabs">
<Tab
icon={listIcon}
label="List"
display="list"
tab=""
current={currentTab}
setDisplay={setDisplay}
/>
<Tab
icon={worldIcon}
label="Map"
display="map"
tab="map"
current={currentTab}
setDisplay={setDisplay}
/>
<Tab
icon={consensusIcon}
label="Consensus"
display="consensus"
tab="consensus"
current={currentTab}
setDisplay={setDisplay}
/>
<Tab
icon={settingsIcon}
label="Settings"
display="settings"
tab="settings"
current={currentTab}
setDisplay={setDisplay}
/>
</div>
</div>
);
}
}
+6 -7
View File
@@ -1,24 +1,23 @@
.Chain-Tab {
display: inline-block;
}
.Chain-Tab .Icon {
margin-right: 5px;
font-size: 24px;
line-height: 24px;
height: 24px;
width: 24px;
padding: 6px;
color: #555;
cursor: pointer;
padding: 10px;
border-radius: 40px;
transition: background-color 0.15s linear;
}
.Chain-Tab:hover .Icon {
.Chain-Tab:hover {
background: #ccc;
}
.Chain-Tab-on .Icon,
.Chain-Tab-on:hover .Icon {
.Chain-Tab-on,
.Chain-Tab-on:hover {
background: #e6007a;
color: #fff;
}
+2 -2
View File
@@ -23,8 +23,8 @@ export class Tab extends React.Component<Tab.Props, {}> {
const className = highlight ? 'Chain-Tab-on Chain-Tab' : 'Chain-Tab';
return (
<div className={className} onClick={this.onClick}>
<Icon src={icon} alt={label} />
<div className={className} onClick={this.onClick} title={label}>
<Icon src={icon} />
</div>
);
}
+1
View File
@@ -1,2 +1,3 @@
export * from './Chain';
export * from './Tab';
export * from './Header';
+21 -23
View File
@@ -1,28 +1,25 @@
.Chains {
background: #b5aeae;
background: #e6007a;
color: #000;
padding: 0 76px 0 16px;
height: 40px;
min-width: 1318px;
/* min-width is 1350 - 76 - 16 to account for padding */
min-width: 1258px;
position: relative;
}
.Chains-chain {
top: 4px;
padding: 0 12px;
background: #b5aeae;
color: #444;
color: #fff;
display: inline-block;
border-right: 1px solid rgba(255, 255, 255, 0.5);
height: 40px;
line-height: 40px;
margin-right: 4px;
height: 36;
line-height: 36px;
cursor: pointer;
font-size: 0.8em;
font-weight: bold;
position: relative;
}
.Chains-chain:first-child {
border-left: 1px solid rgba(255, 255, 255, 0.5);
border-radius: 4px 4px 0 0;
}
.Chains-all-chains {
@@ -49,27 +46,28 @@
margin: 0;
height: 28px;
width: 28px;
color: #3c3c3b;
color: #fff;
}
.Chains-node-count {
padding: 0 5px;
display: inline-block;
padding: 0 0.5em 0.1em;
border-radius: 1em;
background: #8c8787;
color: #fff;
font-weight: normal;
text-shadow: rgba(0, 0, 0, 0.5) 0 1px 0;
height: 20px;
border-radius: 20px;
background: #fff;
color: #e6007a;
font-size: 0.9em;
line-height: 1.4em;
margin: 0 -0.3em 0 0.3em;
line-height: 20px;
margin: 0 -0.5em 0 0.5em;
}
.Chains-chain-selected {
background: #fff;
color: #000;
color: #393838;
font-weight: bold;
}
.Chains-chain-selected .Chains-node-count {
background: #e6007a;
background: #393838;
color: #fff;
}
+42 -10
View File
@@ -5,7 +5,7 @@ import { Types, Maybe } from '../common';
import { ChainData } from '../state';
import githubIcon from '../icons/mark-github.svg';
import listIcon from '../icons/three-bars.svg';
import listIcon from '../icons/kebab-horizontal.svg';
import './Chains.css';
export namespace Chains {
@@ -16,8 +16,29 @@ export namespace Chains {
}
}
// How many chains should be rendered in the DOM
const VISIBLE_CAP = 16;
// Milliseconds, sets the minimum time between the renders
const RENDER_THROTTLE = 1000;
export class Chains extends React.Component<Chains.Props, {}> {
private lastRender = performance.now();
private clicked: Maybe<Types.ChainLabel>;
public shouldComponentUpdate(nextProps: Chains.Props) {
if (nextProps.subscribed !== this.clicked) {
this.clicked = nextProps.subscribed;
}
return (
this.props.subscribed !== nextProps.subscribed ||
performance.now() - this.lastRender > RENDER_THROTTLE
);
}
public render() {
this.lastRender = performance.now();
const allChainsHref = this.props.subscribed
? `#all-chains/${this.props.subscribed}`
: `#all-chains`;
@@ -25,16 +46,21 @@ export class Chains extends React.Component<Chains.Props, {}> {
return (
<div className="Chains">
{chains.map((chain) => this.renderChain(chain))}
<a className="Chains-all-chains" href={allChainsHref}>
<Icon src={listIcon} alt="All Chains" />
{chains.slice(0, VISIBLE_CAP).map((chain) => this.renderChain(chain))}
<a
className="Chains-all-chains"
href={allChainsHref}
title="All Chains"
>
<Icon src={listIcon} />
</a>
<a
className="Chains-fork-me"
href="https://github.com/paritytech/substrate-telemetry"
target="_blank"
title="Fork Me!"
>
<Icon src={githubIcon} alt="Fork Me!" />
<Icon src={githubIcon} />
</a>
</div>
);
@@ -43,10 +69,11 @@ export class Chains extends React.Component<Chains.Props, {}> {
private renderChain(chain: ChainData): React.ReactNode {
const { label, nodeCount } = chain;
const className =
label === this.props.subscribed
? 'Chains-chain Chains-chain-selected'
: 'Chains-chain';
let className = 'Chains-chain';
if (label === this.props.subscribed) {
className += ' Chains-chain-selected';
}
return (
<a
@@ -54,7 +81,7 @@ export class Chains extends React.Component<Chains.Props, {}> {
className={className}
onClick={this.subscribe.bind(this, label)}
>
{label}{' '}
{label}
<span className="Chains-node-count" title="Node Count">
{nodeCount}
</span>
@@ -63,6 +90,11 @@ export class Chains extends React.Component<Chains.Props, {}> {
}
private async subscribe(chain: Types.ChainLabel) {
if (chain === this.clicked) {
return;
}
this.clicked = chain;
const connection = await this.props.connection;
connection.subscribe(chain);
@@ -195,9 +195,9 @@ export class ConsensusBlock extends React.Component<ConsensusBlock.Props, {}> {
];
finalizedInfo = matrice.ImplicitFinalized ? (
<Icon className="implicit" src={finalizedIcon} alt="" />
<Icon className="implicit" src={finalizedIcon} />
) : (
<Icon className="explicit" src={finalizedIcon} alt="" />
<Icon className="explicit" src={finalizedIcon} />
);
finalizedHash = matrice.FinalizedHash ? (
@@ -301,25 +301,17 @@ export class ConsensusBlock extends React.Component<ConsensusBlock.Props, {}> {
let statPrecommit;
if (implicitPrevote) {
statPrevote = (
<Icon src={checkIcon} className="implicit" alt="Implicit Prevote" />
);
statPrevote = <Icon src={checkIcon} className="implicit" />;
}
if (implicitPrecommit) {
statPrecommit = (
<Icon src={checkIcon} className="implicit" alt="Implicit Precommit" />
);
statPrecommit = <Icon src={checkIcon} className="implicit" />;
}
if (prevote) {
statPrevote = (
<Icon src={checkIcon} className="explicit" alt="Prevote" />
);
statPrevote = <Icon src={checkIcon} className="explicit" />;
}
if (precommit) {
statPrecommit = (
<Icon src={checkIcon} className="explicit" alt="Precommit" />
);
statPrecommit = <Icon src={checkIcon} className="explicit" />;
}
return (
@@ -329,7 +321,7 @@ export class ConsensusBlock extends React.Component<ConsensusBlock.Props, {}> {
</span>
);
} else {
return <Icon src={hatchingIcon} className="hatching" alt="" />;
return <Icon src={hatchingIcon} className="hatching" />;
}
}
}
+1 -1
View File
@@ -26,7 +26,7 @@ export class Filter extends React.Component<Filter.Props, {}> {
private filterInput: HTMLInputElement;
public componentWillMount() {
public componentDidMount() {
window.addEventListener('keyup', this.onWindowKeyUp);
}
+2 -2
View File
@@ -8,7 +8,7 @@
display: inline-block;
}
.Icon svg {
width: auto;
.Icon svg, .Icon-symbol-root symbol {
width: 1em;
height: 1em;
}
+57 -18
View File
@@ -1,36 +1,75 @@
import * as React from 'react';
import ReactSVG from 'react-svg';
import './Icon.css';
import { getSVGShadowRoot, W3SVG } from '../utils';
export interface Props {
src: string;
alt?: string;
className?: string;
onClick?: () => void;
export namespace Icon {
export interface Props {
src: string;
className?: string;
onClick?: () => void;
}
}
export class Icon extends React.Component<{}, Props> {
public props: Props;
const symbols = new Map<string, string>();
public shouldComponentUpdate(nextProps: Props) {
let symbolId = 0;
// Lazily render the icon in the DOM, so that we can referenced
// it by id using shadow DOM.
function renderShadowIcon(src: string): string {
let symbol = symbols.get(src);
if (!symbol) {
symbol = `icon${symbolId}`;
symbolId += 1;
symbols.set(src, symbol);
fetch(src).then(async (response) => {
const html = await response.text();
const temp = document.createElement('div');
temp.innerHTML = html;
const tempSVG = temp.querySelector('svg') as SVGSVGElement;
const symEl = document.createElementNS(W3SVG, 'symbol');
const viewBox = tempSVG.getAttribute('viewBox');
symEl.setAttribute('id', symbol as string);
if (viewBox) {
symEl.setAttribute('viewBox', viewBox);
}
for (const child of Array.from(tempSVG.childNodes)) {
symEl.appendChild(child);
}
getSVGShadowRoot().appendChild(symEl);
});
}
return symbol;
}
export class Icon extends React.Component<Icon.Props, {}> {
public props: Icon.Props;
public shouldComponentUpdate(nextProps: Icon.Props) {
return (
this.props.src !== nextProps.src ||
this.props.alt !== nextProps.alt ||
this.props.className !== nextProps.className
);
}
public render() {
const { alt, className, onClick, src } = this.props;
const { className, onClick, src } = this.props;
const symbol = renderShadowIcon(src);
// Use `href` for a shadow DOM reference to the rendered icon
return (
<ReactSVG
key={this.props.src}
title={alt}
className={`Icon ${className || ''}`}
path={src}
onClick={onClick}
/>
<svg className={`Icon ${className || ''}`} onClick={onClick}>
<use href={`#${symbol}`} />
</svg>
);
}
}
-410
View File
@@ -1,410 +0,0 @@
import * as React from 'react';
import { Types, Maybe, timestamp } from '../../common';
import { State, Node } from '../../state';
import { Truncate } from './';
import { Ago, Icon, Tooltip, Sparkline, PolkadotIcon } from '../';
import {
formatNumber,
getHashData,
milliOrSecond,
secondsWithPrecision,
} from '../../utils';
export interface Column {
label: string;
icon: string;
width?: number;
setting?: keyof State.Settings;
sortBy?: (node: Node) => any;
render: (node: Node) => React.ReactElement<any> | string;
}
import nodeIcon from '../../icons/server.svg';
import nodeLocationIcon from '../../icons/location.svg';
import nodeValidatorIcon from '../../icons/shield.svg';
import nodeTypeIcon from '../../icons/terminal.svg';
import networkIdIcon from '../../icons/fingerprint.svg';
import peersIcon from '../../icons/broadcast.svg';
import transactionsIcon from '../../icons/inbox.svg';
import blockIcon from '../../icons/cube.svg';
import finalizedIcon from '../../icons/cube-alt.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 uploadIcon from '../../icons/cloud-upload.svg';
import downloadIcon from '../../icons/cloud-download.svg';
import stateIcon from '../../icons/git-branch.svg';
import networkIcon from '../../icons/network.svg';
import uptimeIcon from '../../icons/pulse.svg';
import externalLinkIcon from '../../icons/link-external.svg';
import parityPolkadotIcon from '../../icons/dot.svg';
import paritySubstrateIcon from '../../icons/substrate.svg';
import polkadotJsIcon from '../../icons/polkadot-js.svg';
import airalabRobonomicsIcon from '../../icons/robonomics.svg';
import chainXIcon from '../../icons/chainx.svg';
import edgewareIcon from '../../icons/edgeware.svg';
import joystreamIcon from '../../icons/joystream.svg';
import ladderIcon from '../../icons/laddernetwork.svg';
import cennznetIcon from '../../icons/cennznet.svg';
import crabIcon from '../../icons/crab.svg';
import darwiniaIcon from '../../icons/darwinia.svg';
import turingIcon from '../../icons/turingnetwork.svg';
import dothereumIcon from '../../icons/dothereum.svg';
import katalchainIcon from '../../icons/katalchain.svg';
import bifrostIcon from '../../icons/bifrost.svg';
import totemIcon from '../../icons/totem.svg';
import nodleIcon from '../../icons/nodle.svg';
import zeroIcon from '../../icons/zero.svg';
import unknownImplementationIcon from '../../icons/question-solid.svg';
const ICONS = {
'parity-polkadot': parityPolkadotIcon,
'Parity Polkadot': parityPolkadotIcon,
'polkadot-js': polkadotJsIcon,
'airalab-robonomics': airalabRobonomicsIcon,
'substrate-node': paritySubstrateIcon,
'Substrate Node': paritySubstrateIcon,
'edgeware-node': edgewareIcon,
'Edgeware Node': edgewareIcon,
'joystream-node': joystreamIcon,
ChainX: chainXIcon,
'ladder-node': ladderIcon,
'cennznet-node': cennznetIcon,
'Darwinia Crab': crabIcon,
Darwinia: darwiniaIcon,
'turing-node': turingIcon,
dothereum: dothereumIcon,
katalchain: katalchainIcon,
'bifrost-node': bifrostIcon,
'totem-meccano-node': totemIcon,
Totem: totemIcon,
'Nodle Chain Node': nodleIcon,
subzero: zeroIcon,
};
export namespace Column {
export const NAME: Column = {
label: 'Node',
icon: nodeIcon,
sortBy: ({ sortableName }) => sortableName,
render: ({ name }) => <Truncate text={name} position="left" />,
};
export const VALIDATOR: Column = {
label: 'Validator',
icon: nodeValidatorIcon,
width: 16,
setting: 'validator',
sortBy: ({ validator }) => validator || '',
render: ({ validator }) => {
return validator ? (
<Tooltip text={validator} copy={true}>
<span className="Row-validator">
<PolkadotIcon account={validator} size={16} />
</span>
</Tooltip>
) : (
'-'
);
},
};
export const LOCATION: Column = {
label: 'Location',
icon: nodeLocationIcon,
width: 140,
setting: 'location',
sortBy: ({ city }) => city || '',
render: ({ city }) =>
city ? <Truncate position="left" text={city} /> : '-',
};
export const IMPLEMENTATION: Column = {
label: 'Implementation',
icon: nodeTypeIcon,
width: 90,
setting: 'implementation',
sortBy: ({ sortableVersion }) => sortableVersion,
render: ({ implementation, version }) => {
const [semver] = version.match(SEMVER_PATTERN) || ['?.?.?'];
const implIcon = ICONS[implementation] || unknownImplementationIcon;
return (
<Tooltip text={`${implementation} v${version}`}>
<Icon src={implIcon} /> {semver}
</Tooltip>
);
},
};
export const NETWORK_ID: Column = {
label: 'Network ID',
icon: networkIdIcon,
width: 90,
setting: 'networkId',
sortBy: ({ networkId }) => networkId || '',
render: ({ networkId }) =>
networkId ? <Truncate position="left" text={networkId} /> : '-',
};
export const PEERS: Column = {
label: 'Peer Count',
icon: peersIcon,
width: 26,
setting: 'peers',
sortBy: ({ peers }) => peers,
render: ({ peers }) => `${peers}`,
};
export const TXS: Column = {
label: 'Transactions in Queue',
icon: transactionsIcon,
width: 26,
setting: 'txs',
sortBy: ({ txs }) => txs,
render: ({ txs }) => `${txs}`,
};
export const UPLOAD: Column = {
label: 'Upload Bandwidth',
icon: uploadIcon,
width: 40,
setting: 'upload',
sortBy: ({ upload }) => (upload.length < 3 ? 0 : upload[upload.length - 1]),
render: ({ upload, chartstamps }) => {
if (upload.length < 3) {
return '-';
}
return (
<Sparkline
width={44}
height={16}
stroke={1}
format={formatBandwidth}
values={upload}
stamps={chartstamps}
minScale={BANDWIDTH_SCALE}
/>
);
},
};
export const DOWNLOAD: Column = {
label: 'Download Bandwidth',
icon: downloadIcon,
width: 40,
setting: 'download',
sortBy: ({ download }) =>
download.length < 3 ? 0 : download[download.length - 1],
render: ({ download, chartstamps }) => {
if (download.length < 3) {
return '-';
}
return (
<Sparkline
width={44}
height={16}
stroke={1}
format={formatBandwidth}
values={download}
stamps={chartstamps}
minScale={BANDWIDTH_SCALE}
/>
);
},
};
export const STATE_CACHE: Column = {
label: 'State Cache Size',
icon: stateIcon,
width: 40,
setting: 'stateCacheSize',
sortBy: ({ stateCacheSize }) =>
stateCacheSize.length < 3 ? 0 : stateCacheSize[stateCacheSize.length - 1],
render: ({ stateCacheSize, chartstamps }) => {
if (stateCacheSize.length < 3) {
return '-';
}
return (
<Sparkline
width={44}
height={16}
stroke={1}
format={formatBytes}
values={stateCacheSize}
stamps={chartstamps}
minScale={MEMORY_SCALE}
/>
);
},
};
export const BLOCK_NUMBER: Column = {
label: 'Block',
icon: blockIcon,
width: 88,
setting: 'blocknumber',
sortBy: ({ height }) => height || 0,
render: ({ height }) => `#${formatNumber(height)}`,
};
export const BLOCK_HASH: Column = {
label: 'Block Hash',
icon: blockHashIcon,
width: 154,
setting: 'blockhash',
sortBy: ({ hash }) => hash || '',
render: ({ hash }) => <Truncate position="right" text={hash} copy={true} />,
};
export const FINALIZED: Column = {
label: 'Finalized Block',
icon: finalizedIcon,
width: 88,
setting: 'finalized',
sortBy: ({ finalized }) => finalized || 0,
render: ({ finalized }) => `#${formatNumber(finalized)}`,
};
export const FINALIZED_HASH: Column = {
label: 'Finalized Block Hash',
icon: blockHashIcon,
width: 154,
setting: 'finalizedhash',
sortBy: ({ finalizedHash }) => finalizedHash || '',
render: ({ finalizedHash }) => (
<Truncate position="right" text={finalizedHash} copy={true} />
),
};
export const BLOCK_TIME: Column = {
label: 'Block Time',
icon: blockTimeIcon,
width: 80,
setting: 'blocktime',
sortBy: ({ blockTime }) => (blockTime == null ? Infinity : blockTime),
render: ({ blockTime }) => `${secondsWithPrecision(blockTime / 1000)}`,
};
export const BLOCK_PROPAGATION: Column = {
label: 'Block Propagation Time',
icon: propagationTimeIcon,
width: 58,
setting: 'blockpropagation',
sortBy: ({ propagationTime }) =>
propagationTime == null ? Infinity : propagationTime,
render: ({ propagationTime }) =>
propagationTime == null ? '∞' : milliOrSecond(propagationTime),
};
export const BLOCK_LAST_TIME: Column = {
label: 'Last Block Time',
icon: lastTimeIcon,
width: 100,
setting: 'blocklasttime',
sortBy: ({ blockTimestamp }) => blockTimestamp || 0,
render: ({ blockTimestamp }) => <Ago when={blockTimestamp} />,
};
export const UPTIME: Column = {
label: 'Node Uptime',
icon: uptimeIcon,
width: 58,
setting: 'uptime',
sortBy: ({ connectedAt }) => connectedAt || 0,
render: ({ connectedAt }) => <Ago when={connectedAt} justTime={true} />,
};
export const NETWORK_STATE: Column = {
label: 'NetworkState',
icon: networkIcon,
width: 16,
setting: 'networkstate',
render: ({ id }) => {
const chainLabel = getHashData().chain;
if (!chainLabel) {
return '-';
}
const uri = `${URI_BASE}${encodeURIComponent(chainLabel)}/${id}/`;
return (
<a href={uri} target="_blank">
<Icon src={externalLinkIcon} />
</a>
);
},
};
}
const SEMVER_PATTERN = /^\d+\.\d+\.\d+/;
const BANDWIDTH_SCALE = 1024 * 1024;
const MEMORY_SCALE = 2 * 1024 * 1024;
const URI_BASE =
window.location.protocol === 'https:'
? `/network_state/`
: `http://${window.location.hostname}:8000/network_state/`;
function formatStamp(stamp: Types.Timestamp): string {
const passed = ((timestamp() - stamp) / 1000) | 0;
const hours = (passed / 3600) | 0;
const minutes = ((passed % 3600) / 60) | 0;
const seconds = passed % 60 | 0;
return hours
? `${hours}h ago`
: minutes
? `${minutes}m ago`
: `${seconds}s ago`;
}
function formatMemory(kbs: number, stamp: Maybe<Types.Timestamp>): string {
const ago = stamp ? ` (${formatStamp(stamp)})` : '';
const mbs = (kbs / 1024) | 0;
if (mbs >= 1000) {
return `${(mbs / 1024).toFixed(1)} GB${ago}`;
} else {
return `${mbs} MB${ago}`;
}
}
function formatBytes(bytes: number, stamp: Maybe<Types.Timestamp>): string {
const ago = stamp ? ` (${formatStamp(stamp)})` : '';
if (bytes >= 1024 * 1024 * 1024) {
return `${(bytes / (1024 * 1024 * 1024)).toFixed(1)} GB${ago}`;
} else if (bytes >= 1024 * 1024) {
return `${(bytes / (1024 * 1024)).toFixed(1)} MB${ago}`;
} else if (bytes >= 1000) {
return `${(bytes / 1024).toFixed(1)} kB${ago}`;
} else {
return `${bytes} B${ago}`;
}
}
function formatBandwidth(bps: number, stamp: Maybe<Types.Timestamp>): string {
const ago = stamp ? ` (${formatStamp(stamp)})` : '';
if (bps >= 1024 * 1024) {
return `${(bps / (1024 * 1024)).toFixed(1)} MB/s${ago}`;
} else if (bps >= 1000) {
return `${(bps / 1024).toFixed(1)} kB/s${ago}`;
} else {
return `${bps | 0} B/s${ago}`;
}
}
function formatCPU(cpu: number, stamp: Maybe<Types.Timestamp>): string {
const ago = stamp ? ` (${formatStamp(stamp)})` : '';
const fractionDigits = cpu > 100 ? 0 : cpu > 10 ? 1 : cpu > 1 ? 2 : 3;
return `${cpu.toFixed(fractionDigits)}%${ago}`;
}
@@ -0,0 +1,46 @@
import * as React from 'react';
import { Maybe } from '../../../common';
import { Column } from './';
import { Node } from '../../../state';
import { Truncate, Tooltip } from '../../';
import icon from '../../../icons/file-binary.svg';
export class BlockHashColumn extends React.Component<Column.Props, {}> {
public static readonly label = 'Block Hash';
public static readonly icon = icon;
public static readonly width = 154;
public static readonly setting = 'blockhash';
public static readonly sortBy = ({ hash }: Node) => hash || '';
private data: Maybe<string>;
private copy: Maybe<Tooltip.CopyCallback>;
public shouldComponentUpdate(nextProps: Column.Props) {
return this.data !== nextProps.node.hash;
}
render() {
const { hash } = this.props.node;
this.data = hash;
return (
<td className="Column" onClick={this.onClick}>
<Tooltip text={hash} position="right" copy={this.onCopy} />
<Truncate text={hash} chars={16} />
</td>
);
}
private onCopy = (copy: Tooltip.CopyCallback) => {
this.copy = copy;
};
private onClick = (event: React.MouseEvent) => {
event.stopPropagation();
if (this.copy != null) {
this.copy();
}
};
}
@@ -0,0 +1,27 @@
import * as React from 'react';
import { Column } from './';
import { Node } from '../../../state';
import { formatNumber } from '../../../utils';
import icon from '../../../icons/cube.svg';
export class BlockNumberColumn extends React.Component<Column.Props, {}> {
public static readonly label = 'Block';
public static readonly icon = icon;
public static readonly width = 88;
public static readonly setting = 'blocknumber';
public static readonly sortBy = ({ height }: Node) => height;
private data = 0;
public shouldComponentUpdate(nextProps: Column.Props) {
return this.data !== nextProps.node.height;
}
render() {
const { height } = this.props.node;
this.data = height;
return <td className="Column">{`#${formatNumber(height)}`}</td>;
}
}
@@ -0,0 +1,31 @@
import * as React from 'react';
import { Maybe } from '../../../common';
import { Column } from './';
import { Node } from '../../../state';
import { milliOrSecond } from '../../../utils';
import icon from '../../../icons/dashboard.svg';
export class BlockPropagationColumn extends React.Component<Column.Props, {}> {
public static readonly label = 'Block Propagation Time';
public static readonly icon = icon;
public static readonly width = 58;
public static readonly setting = 'blockpropagation';
public static readonly sortBy = ({ propagationTime }: Node) =>
propagationTime == null ? Infinity : propagationTime;
private data: Maybe<number>;
public shouldComponentUpdate(nextProps: Column.Props) {
return this.data !== nextProps.node.propagationTime;
}
render() {
const { propagationTime } = this.props.node;
const print =
propagationTime == null ? '∞' : milliOrSecond(propagationTime);
this.data = propagationTime;
return <td className="Column">{print}</td>;
}
}
@@ -0,0 +1,30 @@
import * as React from 'react';
import { Column } from './';
import { Node } from '../../../state';
import { secondsWithPrecision } from '../../../utils';
import icon from '../../../icons/history.svg';
export class BlockTimeColumn extends React.Component<Column.Props, {}> {
public static readonly label = 'Block Time';
public static readonly icon = icon;
public static readonly width = 80;
public static readonly setting = 'blocktime';
public static readonly sortBy = ({ blockTime }: Node) =>
blockTime == null ? Infinity : blockTime;
private data = 0;
public shouldComponentUpdate(nextProps: Column.Props) {
return this.data !== nextProps.node.blockTime;
}
render() {
const { blockTime } = this.props.node;
this.data = blockTime;
return (
<td className="Column">{`${secondsWithPrecision(blockTime / 1000)}`}</td>
);
}
}
@@ -0,0 +1,43 @@
.Column {
text-align: left;
padding: 6px 13px;
height: 19px;
position: relative;
white-space: nowrap;
}
.Column-truncate {
position: absolute;
left: 0;
right: 0;
top: 0;
padding: 6px 13px;
overflow: hidden;
text-overflow: ellipsis;
}
.Column-Tooltip {
position: initial !important;
padding: inherit !important;
}
.Column-validator {
display: block;
width: 16px;
height: 16px;
cursor: pointer;
}
.Column-validator:hover {
transform: scale(2);
}
.Column--a {
color: inherit;
text-decoration: none;
}
.Column--a:hover {
text-decoration: underline;
}
@@ -0,0 +1,102 @@
import * as React from 'react';
import { Types, Maybe, timestamp } from '../../../common';
import { Node } from '../../../state';
import './Column.css';
import {
NameColumn,
ValidatorColumn,
LocationColumn,
ImplementationColumn,
NetworkIdColumn,
PeersColumn,
TxsColumn,
UploadColumn,
DownloadColumn,
StateCacheColumn,
BlockNumberColumn,
BlockHashColumn,
FinalizedBlockColumn,
FinalizedHashColumn,
BlockTimeColumn,
BlockPropagationColumn,
LastBlockColumn,
UptimeColumn,
NetworkStateColumn,
} from './';
export type Column =
| typeof NameColumn
| typeof ValidatorColumn
| typeof LocationColumn
| typeof ImplementationColumn
| typeof NetworkIdColumn
| typeof PeersColumn
| typeof TxsColumn
| typeof UploadColumn
| typeof DownloadColumn
| typeof StateCacheColumn
| typeof BlockNumberColumn
| typeof BlockHashColumn
| typeof FinalizedBlockColumn
| typeof FinalizedHashColumn
| typeof BlockTimeColumn
| typeof BlockPropagationColumn
| typeof LastBlockColumn
| typeof UptimeColumn
| typeof NetworkStateColumn;
export namespace Column {
export interface Props {
node: Node;
}
export function formatBytes(
bytes: number,
stamp: Maybe<Types.Timestamp>
): string {
const ago = stamp ? ` (${formatStamp(stamp)})` : '';
if (bytes >= 1024 * 1024 * 1024) {
return `${(bytes / (1024 * 1024 * 1024)).toFixed(1)} GB${ago}`;
} else if (bytes >= 1024 * 1024) {
return `${(bytes / (1024 * 1024)).toFixed(1)} MB${ago}`;
} else if (bytes >= 1000) {
return `${(bytes / 1024).toFixed(1)} kB${ago}`;
} else {
return `${bytes} B${ago}`;
}
}
export function formatBandwidth(
bps: number,
stamp: Maybe<Types.Timestamp>
): string {
const ago = stamp ? ` (${formatStamp(stamp)})` : '';
if (bps >= 1024 * 1024) {
return `${(bps / (1024 * 1024)).toFixed(1)} MB/s${ago}`;
} else if (bps >= 1000) {
return `${(bps / 1024).toFixed(1)} kB/s${ago}`;
} else {
return `${bps | 0} B/s${ago}`;
}
}
}
export const BANDWIDTH_SCALE = 1024 * 1024;
function formatStamp(stamp: Types.Timestamp): string {
const passed = ((timestamp() - stamp) / 1000) | 0;
const hours = (passed / 3600) | 0;
const minutes = ((passed % 3600) / 60) | 0;
const seconds = passed % 60 | 0;
return hours
? `${hours}h ago`
: minutes
? `${minutes}m ago`
: `${seconds}s ago`;
}
@@ -0,0 +1,46 @@
import * as React from 'react';
import { Types, Maybe, timestamp } from '../../../common';
import { Column, BANDWIDTH_SCALE } from './';
import { Node } from '../../../state';
import { Sparkline } from '../../';
import icon from '../../../icons/cloud-download.svg';
export class DownloadColumn extends React.Component<Column.Props, {}> {
public static readonly label = 'Download Bandwidth';
public static readonly icon = icon;
public static readonly width = 40;
public static readonly setting = 'download';
public static readonly sortBy = ({ download }: Node) =>
download.length < 3 ? 0 : download[download.length - 1];
private data: Array<number> = [];
public shouldComponentUpdate(nextProps: Column.Props) {
// Diffing by ref, as data is an immutable array
return this.data !== nextProps.node.download;
}
render() {
const { download, chartstamps } = this.props.node;
this.data = download;
if (download.length < 3) {
return <td className="Column">-</td>;
}
return (
<td className="Column">
<Sparkline
width={44}
height={16}
stroke={1}
format={Column.formatBandwidth}
values={download}
stamps={chartstamps}
minScale={BANDWIDTH_SCALE}
/>
</td>
);
}
}
@@ -0,0 +1,27 @@
import * as React from 'react';
import { Column } from './';
import { Node } from '../../../state';
import { formatNumber } from '../../../utils';
import icon from '../../../icons/cube-alt.svg';
export class FinalizedBlockColumn extends React.Component<Column.Props, {}> {
public static readonly label = 'Finalized Block';
public static readonly icon = icon;
public static readonly width = 88;
public static readonly setting = 'finalized';
public static readonly sortBy = ({ finalized }: Node) => finalized || 0;
private data = 0;
public shouldComponentUpdate(nextProps: Column.Props) {
return this.data !== nextProps.node.finalized;
}
render() {
const { finalized } = this.props.node;
this.data = finalized;
return <td className="Column">{`#${formatNumber(finalized)}`}</td>;
}
}
@@ -0,0 +1,47 @@
import * as React from 'react';
import { Maybe } from '../../../common';
import { Column } from './';
import { Node } from '../../../state';
import { Truncate, Tooltip } from '../../';
import icon from '../../../icons/file-binary.svg';
export class FinalizedHashColumn extends React.Component<Column.Props, {}> {
public static readonly label = 'Finalized Block Hash';
public static readonly icon = icon;
public static readonly width = 154;
public static readonly setting = 'finalizedhash';
public static readonly sortBy = ({ finalizedHash }: Node) =>
finalizedHash || '';
private data: Maybe<string>;
private copy: Maybe<Tooltip.CopyCallback>;
public shouldComponentUpdate(nextProps: Column.Props) {
return this.data !== nextProps.node.finalizedHash;
}
render() {
const { finalizedHash } = this.props.node;
this.data = finalizedHash;
return (
<td className="Column" onClick={this.onClick}>
<Tooltip text={finalizedHash} position="right" copy={this.onCopy} />
<Truncate text={finalizedHash} chars={16} />
</td>
);
}
private onCopy = (copy: Tooltip.CopyCallback) => {
this.copy = copy;
};
private onClick = (event: React.MouseEvent) => {
event.stopPropagation();
if (this.copy != null) {
this.copy();
}
};
}
@@ -0,0 +1,91 @@
import * as React from 'react';
import { Column } from './';
import { Node } from '../../../state';
import { Tooltip, Icon } from '../../';
import icon from '../../../icons/terminal.svg';
import parityPolkadotIcon from '../../../icons/dot.svg';
import paritySubstrateIcon from '../../../icons/substrate.svg';
import polkadotJsIcon from '../../../icons/polkadot-js.svg';
import airalabRobonomicsIcon from '../../../icons/robonomics.svg';
import chainXIcon from '../../../icons/chainx.svg';
import edgewareIcon from '../../../icons/edgeware.svg';
import joystreamIcon from '../../../icons/joystream.svg';
import ladderIcon from '../../../icons/laddernetwork.svg';
import cennznetIcon from '../../../icons/cennznet.svg';
import crabIcon from '../../../icons/crab.svg';
import darwiniaIcon from '../../../icons/darwinia.svg';
import turingIcon from '../../../icons/turingnetwork.svg';
import dothereumIcon from '../../../icons/dothereum.svg';
import katalchainIcon from '../../../icons/katalchain.svg';
import bifrostIcon from '../../../icons/bifrost.svg';
import totemIcon from '../../../icons/totem.svg';
import nodleIcon from '../../../icons/nodle.svg';
import zeroIcon from '../../../icons/zero.svg';
const ICONS = {
'parity-polkadot': parityPolkadotIcon,
'Parity Polkadot': parityPolkadotIcon,
'polkadot-js': polkadotJsIcon,
'airalab-robonomics': airalabRobonomicsIcon,
'substrate-node': paritySubstrateIcon,
'Substrate Node': paritySubstrateIcon,
'edgeware-node': edgewareIcon,
'Edgeware Node': edgewareIcon,
'joystream-node': joystreamIcon,
ChainX: chainXIcon,
'ladder-node': ladderIcon,
'cennznet-node': cennznetIcon,
'Darwinia Crab': crabIcon,
Darwinia: darwiniaIcon,
'turing-node': turingIcon,
dothereum: dothereumIcon,
katalchain: katalchainIcon,
'bifrost-node': bifrostIcon,
'totem-meccano-node': totemIcon,
Totem: totemIcon,
'Nodle Chain Node': nodleIcon,
subzero: zeroIcon,
};
const SEMVER_PATTERN = /^\d+\.\d+\.\d+/;
export class ImplementationColumn extends React.Component<Column.Props, {}> {
public static readonly label = 'Implementation';
public static readonly icon = icon;
public static readonly width = 90;
public static readonly setting = 'implementation';
public static readonly sortBy = ({ sortableVersion }: Node) =>
sortableVersion;
private implementation: string;
private version: string;
public shouldComponentUpdate(nextProps: Column.Props) {
if (this.props.node === nextProps.node) {
// Implementation can't change unless we got a new node
return false;
}
return (
this.implementation !== nextProps.node.implementation ||
this.version !== nextProps.node.version
);
}
render() {
const { implementation, version } = this.props.node;
this.implementation = implementation;
this.version = version;
const [semver] = version.match(SEMVER_PATTERN) || ['?.?.?'];
const implIcon = ICONS[implementation] || paritySubstrateIcon;
return (
<td className="Column">
<Tooltip text={`${implementation} v${version}`} />
<Icon src={implIcon} /> {semver}
</td>
);
}
}
@@ -0,0 +1,32 @@
import * as React from 'react';
import { Column } from './';
import { Node } from '../../../state';
import { Ago } from '../../';
import icon from '../../../icons/watch.svg';
export class LastBlockColumn extends React.Component<Column.Props, {}> {
public static readonly label = 'Last Block Time';
public static readonly icon = icon;
public static readonly width = 100;
public static readonly setting = 'blocklasttime';
public static readonly sortBy = ({ blockTimestamp }: Node) =>
blockTimestamp || 0;
private data = 0;
public shouldComponentUpdate(nextProps: Column.Props) {
return this.data !== nextProps.node.blockTimestamp;
}
render() {
const { blockTimestamp } = this.props.node;
this.data = blockTimestamp;
return (
<td className="Column">
<Ago when={blockTimestamp} />
</td>
);
}
}
@@ -0,0 +1,37 @@
import * as React from 'react';
import { Maybe } from '../../../common';
import { Column } from './';
import { Node } from '../../../state';
import { Truncate, Tooltip } from '../../';
import icon from '../../../icons/location.svg';
export class LocationColumn extends React.Component<Column.Props, {}> {
public static readonly label = 'Location';
public static readonly icon = icon;
public static readonly width = 140;
public static readonly setting = 'location';
public static readonly sortBy = ({ city }: Node) => city || '';
private data: Maybe<string>;
public shouldComponentUpdate(nextProps: Column.Props) {
return this.data !== nextProps.node.city;
}
render() {
const { city } = this.props.node;
this.data = city;
if (!city) {
return <td className="Column">-</td>;
}
return (
<td className="Column">
<Tooltip text={city} position="left" />
<Truncate text={city} chars={14} />
</td>
);
}
}
@@ -0,0 +1,29 @@
import * as React from 'react';
import { Column } from './';
import { Node } from '../../../state';
import { Truncate, Tooltip } from '../../';
import icon from '../../../icons/server.svg';
export class NameColumn extends React.Component<Column.Props, {}> {
public static readonly label = 'Node';
public static readonly icon = icon;
public static readonly setting = null;
public static readonly width = null;
public static readonly sortBy = ({ sortableName }: Node) => sortableName;
public shouldComponentUpdate(nextProps: Column.Props) {
// Node name only changes when the node does
return this.props.node !== nextProps.node;
}
render() {
const { name } = this.props.node;
return (
<td className="Column">
<Tooltip text={name} position="left" />
<Truncate text={name} />
</td>
);
}
}
@@ -0,0 +1,38 @@
import * as React from 'react';
import { Maybe } from '../../../common';
import { Column } from './';
import { Node } from '../../../state';
import { Truncate } from '../../';
import { Tooltip } from '../../';
import icon from '../../../icons/fingerprint.svg';
export class NetworkIdColumn extends React.Component<Column.Props, {}> {
public static readonly label = 'Network ID';
public static readonly icon = icon;
public static readonly width = 90;
public static readonly setting = 'networkId';
public static readonly sortBy = ({ networkId }: Node) => networkId || '';
private data: Maybe<string>;
public shouldComponentUpdate(nextProps: Column.Props) {
return this.data !== nextProps.node.networkId;
}
render() {
const { networkId } = this.props.node;
this.data = networkId;
if (!networkId) {
return <td className="Column">-</td>;
}
return (
<td className="Column">
<Tooltip text={networkId} position="left" />
<Truncate text={networkId} chars={10} />
</td>
);
}
}
@@ -0,0 +1,44 @@
import * as React from 'react';
import { Column } from './';
import { Node } from '../../../state';
import { Icon } from '../../';
import icon from '../../../icons/network.svg';
import externalLinkIcon from '../../../icons/link-external.svg';
import { getHashData } from '../../../utils';
const URI_BASE =
window.location.protocol === 'https:'
? `/network_state/`
: `http://${window.location.hostname}:8000/network_state/`;
export class NetworkStateColumn extends React.Component<Column.Props, {}> {
public static readonly label = 'Network State';
public static readonly icon = icon;
public static readonly width = 16;
public static readonly setting = 'networkstate';
public static readonly sortBy = null;
public shouldComponentUpdate(nextProps: Column.Props) {
// Network state link changes when the node does
return this.props.node !== nextProps.node;
}
render() {
const { id } = this.props.node;
const chainLabel = getHashData().chain;
if (!chainLabel) {
return <td className="Column">-</td>;
}
const uri = `${URI_BASE}${encodeURIComponent(chainLabel)}/${id}/`;
return (
<td className="Column">
<a className="Column--a" href={uri} target="_blank">
<Icon src={externalLinkIcon} />
</a>
</td>
);
}
}
@@ -0,0 +1,26 @@
import * as React from 'react';
import { Column } from './';
import { Node } from '../../../state';
import icon from '../../../icons/broadcast.svg';
export class PeersColumn extends React.Component<Column.Props, {}> {
public static readonly label = 'Peer Count';
public static readonly icon = icon;
public static readonly width = 26;
public static readonly setting = 'peers';
public static readonly sortBy = ({ peers }: Node) => peers;
private data = 0;
public shouldComponentUpdate(nextProps: Column.Props) {
return this.data !== nextProps.node.peers;
}
render() {
const { peers } = this.props.node;
this.data = peers;
return <td className="Column">{peers}</td>;
}
}
@@ -0,0 +1,46 @@
import * as React from 'react';
import { Types, Maybe, timestamp } from '../../../common';
import { Column, BANDWIDTH_SCALE } from './';
import { Node } from '../../../state';
import { Sparkline } from '../../';
import icon from '../../../icons/git-branch.svg';
export class StateCacheColumn extends React.Component<Column.Props, {}> {
public static readonly label = 'State Cache Size';
public static readonly icon = icon;
public static readonly width = 40;
public static readonly setting = 'stateCacheSize';
public static readonly sortBy = ({ stateCacheSize }: Node) =>
stateCacheSize.length < 3 ? 0 : stateCacheSize[stateCacheSize.length - 1];
private data: Array<number> = [];
public shouldComponentUpdate(nextProps: Column.Props) {
// Diffing by ref, as data is an immutable array
return this.data !== nextProps.node.stateCacheSize;
}
render() {
const { stateCacheSize, chartstamps } = this.props.node;
this.data = stateCacheSize;
if (stateCacheSize.length < 3) {
return <td className="Column">-</td>;
}
return (
<td className="Column">
<Sparkline
width={44}
height={16}
stroke={1}
format={Column.formatBytes}
values={stateCacheSize}
stamps={chartstamps}
minScale={BANDWIDTH_SCALE}
/>
</td>
);
}
}
@@ -0,0 +1,26 @@
import * as React from 'react';
import { Column } from './';
import { Node } from '../../../state';
import icon from '../../../icons/inbox.svg';
export class TxsColumn extends React.Component<Column.Props, {}> {
public static readonly label = 'Transactions in Queue';
public static readonly icon = icon;
public static readonly width = 26;
public static readonly setting = 'txs';
public static readonly sortBy = ({ txs }: Node) => txs;
private data = 0;
public shouldComponentUpdate(nextProps: Column.Props) {
return this.data !== nextProps.node.txs;
}
render() {
const { txs } = this.props.node;
this.data = txs;
return <td className="Column">{txs}</td>;
}
}
@@ -0,0 +1,46 @@
import * as React from 'react';
import { Types, Maybe, timestamp } from '../../../common';
import { Column, BANDWIDTH_SCALE } from './';
import { Node } from '../../../state';
import { Sparkline } from '../../';
import icon from '../../../icons/cloud-upload.svg';
export class UploadColumn extends React.Component<Column.Props, {}> {
public static readonly label = 'Upload Bandwidth';
public static readonly icon = icon;
public static readonly width = 40;
public static readonly setting = 'upload';
public static readonly sortBy = ({ upload }: Node) =>
upload.length < 3 ? 0 : upload[upload.length - 1];
private data: Array<number> = [];
public shouldComponentUpdate(nextProps: Column.Props) {
// Diffing by ref, as data is an immutable array
return this.data !== nextProps.node.upload;
}
render() {
const { upload, chartstamps } = this.props.node;
this.data = upload;
if (upload.length < 3) {
return <td className="Column">-</td>;
}
return (
<td className="Column">
<Sparkline
width={44}
height={16}
stroke={1}
format={Column.formatBandwidth}
values={upload}
stamps={chartstamps}
minScale={BANDWIDTH_SCALE}
/>
</td>
);
}
}
@@ -0,0 +1,28 @@
import * as React from 'react';
import { Column } from './';
import { Node } from '../../../state';
import { Ago } from '../../';
import icon from '../../../icons/pulse.svg';
export class UptimeColumn extends React.Component<Column.Props, {}> {
public static readonly label = 'Node Uptime';
public static readonly icon = icon;
public static readonly width = 58;
public static readonly setting = 'uptime';
public static readonly sortBy = ({ connectedAt }: Node) => connectedAt || 0;
public shouldComponentUpdate(nextProps: Column.Props) {
// Uptime only changes when the node does
return this.props.node !== nextProps.node;
}
render() {
const { connectedAt } = this.props.node;
return (
<td className="Column">
<Ago when={connectedAt} justTime={true} />
</td>
);
}
}
@@ -0,0 +1,54 @@
import * as React from 'react';
import { Maybe } from '../../../common';
import { Column } from './';
import { Node } from '../../../state';
import { Tooltip, PolkadotIcon } from '../../';
import icon from '../../../icons/shield.svg';
export class ValidatorColumn extends React.Component<Column.Props, {}> {
public static readonly label = 'Validator';
public static readonly icon = icon;
public static readonly width = 16;
public static readonly setting = 'validator';
public static readonly sortBy = ({ validator }: Node) => validator || '';
private data: Maybe<string>;
private copy: Maybe<Tooltip.CopyCallback>;
public shouldComponentUpdate(nextProps: Column.Props) {
return this.data !== nextProps.node.validator;
}
render() {
const { validator } = this.props.node;
this.data = validator;
if (!validator) {
return <td className="Column">-</td>;
}
return (
<td className="Column" onClick={this.onClick}>
<Tooltip text={validator} copy={this.onCopy} />
<PolkadotIcon
className="Column-validator"
account={validator}
size={16}
/>
</td>
);
}
private onCopy = (copy: Tooltip.CopyCallback) => {
this.copy = copy;
};
private onClick = (event: React.MouseEvent) => {
event.stopPropagation();
if (this.copy != null) {
this.copy();
}
};
}
@@ -0,0 +1,20 @@
export * from './Column';
export * from './NameColumn';
export * from './ValidatorColumn';
export * from './LocationColumn';
export * from './ImplementationColumn';
export * from './NetworkIdColumn';
export * from './PeersColumn';
export * from './TxsColumn';
export * from './UploadColumn';
export * from './DownloadColumn';
export * from './StateCacheColumn';
export * from './BlockNumberColumn';
export * from './BlockHashColumn';
export * from './FinalizedBlockColumn';
export * from './FinalizedHashColumn';
export * from './BlockTimeColumn';
export * from './BlockPropagationColumn';
export * from './LastBlockColumn';
export * from './UptimeColumn';
export * from './NetworkStateColumn';
+6 -8
View File
@@ -11,15 +11,13 @@
font-weight: 300;
}
.List table {
.List-padding {
padding: 0;
margin: 0;
}
.List--table {
width: 100%;
border-spacing: 0;
}
.List thead {
background: #393838;
}
.List tbody {
font-family: monospace, sans-serif;
}
+82 -19
View File
@@ -1,8 +1,8 @@
import * as React from 'react';
import { Types, Maybe } from '../../common';
import { Filter } from '../';
import { State as AppState, Node } from '../../state';
import { Row } from './';
import { State as AppState, Update as AppUpdate, Node } from '../../state';
import { Row, THead } from './';
import { Persistent, PersistentSet } from '../../persist';
import { viewport } from '../../utils';
@@ -16,6 +16,7 @@ import './List.css';
export namespace List {
export interface Props {
appState: Readonly<AppState>;
appUpdate: AppUpdate;
pins: PersistentSet<Types.NodeName>;
sortBy: Persistent<Maybe<number>>;
}
@@ -23,20 +24,24 @@ export namespace List {
export interface State {
filter: Maybe<(node: Node) => boolean>;
viewportHeight: number;
listStart: number;
listEnd: number;
}
}
// Helper for readability, used as `key` prop for each `Row`
// of the `List`, so that we can maximize re-using DOM elements.
type Key = number;
export class List extends React.Component<List.Props, {}> {
public state = {
filter: null,
viewportHeight: viewport().height,
listStart: 0,
listEnd: 0,
};
private listStart = 0;
private listEnd = 0;
private relativeTop = -1;
private nextKey: Key = 0;
private previousKeys = new Map<Types.NodeId, Key>();
public componentDidMount() {
this.onScroll();
@@ -53,7 +58,7 @@ export class List extends React.Component<List.Props, {}> {
public render() {
const { pins, sortBy, appState } = this.props;
const { selectedColumns } = appState;
const { filter, listStart, listEnd } = this.state;
const { filter } = this.state;
let nodes = appState.nodes.sorted();
@@ -76,23 +81,26 @@ export class List extends React.Component<List.Props, {}> {
// to rendering view, so we put the whole list in focus
appState.nodes.setFocus(0, nodes.length);
} else {
appState.nodes.setFocus(listStart, listEnd);
appState.nodes.setFocus(this.listStart, this.listEnd);
}
const height = TH_HEIGHT + nodes.length * TR_HEIGHT;
const transform = `translateY(${listStart * TR_HEIGHT}px)`;
const top = this.listStart * TR_HEIGHT;
nodes = nodes.slice(listStart, listEnd);
nodes = nodes.slice(this.listStart, this.listEnd);
const keys = this.recalculateKeys(nodes);
return (
<React.Fragment>
<>
<div className="List" style={{ height }}>
<table>
<Row.HEADER columns={selectedColumns} sortBy={sortBy} />
<tbody style={{ transform }}>
{nodes.map((node) => (
<table className="List--table">
<THead columns={selectedColumns} sortBy={sortBy} />
<tbody>
<tr className="List-padding" style={{ height: `${top}px` }} />
{nodes.map((node, i) => (
<Row
key={node.id}
key={keys[i]}
node={node}
pins={pins}
columns={selectedColumns}
@@ -102,10 +110,63 @@ export class List extends React.Component<List.Props, {}> {
</table>
</div>
<Filter onChange={this.onFilterChange} />
</React.Fragment>
</>
);
}
// Get an array of keys for each `Node` in viewport in order.
//
// * If a `Node` was previously rendered, it will keep its `Key`.
//
// * If a `Node` is new to the viewport, it will get a `Key` of
// another `Node` that was removed from the viewport, or a new one.
private recalculateKeys(nodes: Array<Node>): Array<Key> {
// First we find all keys for `Node`s which didn't change from
// last render.
const keptKeys: Array<Maybe<Key>> = nodes.map(({ id }) => {
const key = this.previousKeys.get(id);
if (key != null) {
this.previousKeys.delete(id);
}
return key;
});
// Array of all unused keys
const unusedKeys = Array.from(this.previousKeys.values());
let search = 0;
// Clear the map so we can set new values
this.previousKeys.clear();
// Filling in blanks and re-populate previousKeys
return keptKeys.map((key: Maybe<Key>, i) => {
const id = nodes[i].id;
// `Node` was previously in viewport
if (key != null) {
this.previousKeys.set(id, key);
return key;
}
// Recycle the next unused key
if (search < unusedKeys.length) {
const unused = unusedKeys[search++];
this.previousKeys.set(id, unused);
return unused;
}
// No unused keys left, generate a new key
const newKey = this.nextKey++;
this.previousKeys.set(id, newKey);
return newKey;
});
}
private onScroll = () => {
const relativeTop = divisibleBy(
window.scrollY - (HEADER + TR_HEIGHT),
@@ -125,8 +186,10 @@ export class List extends React.Component<List.Props, {}> {
const listStart = Math.max(((top / TR_HEIGHT) | 0) - ROW_MARGIN, 0);
const listEnd = listStart + ROW_MARGIN * 2 + Math.ceil(height / TR_HEIGHT);
if (listStart !== this.state.listStart || listEnd !== this.state.listEnd) {
this.setState({ listStart, listEnd });
if (listStart !== this.listStart || listEnd !== this.listEnd) {
this.listStart = listStart;
this.listEnd = listEnd;
this.props.appUpdate({});
}
};
+1 -62
View File
@@ -3,56 +3,6 @@
cursor: pointer;
}
.Row a {
color: inherit;
text-decoration: none;
}
.Row a:hover {
text-decoration: underline;
}
.Row-Header th,
.Row td {
text-align: left;
padding: 6px 13px;
height: 19px;
}
.Row td {
position: relative;
}
.Row-Header th {
height: 23px;
}
.Row-Header th.HeaderCell-sortable {
cursor: pointer;
}
.Row-Header th.HeaderCell-sorted {
cursor: pointer;
background: #e6007a;
color: #fff;
}
.Row .Row-truncate {
position: absolute;
left: 0;
right: 0;
top: 0;
padding: inherit;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.Row .Row-Tooltip {
position: initial;
padding: inherit;
}
.Row-synced {
color: #fff;
}
@@ -72,20 +22,9 @@
}
.Row-stale {
font-style: italic;
color: #555;
}
.Row:hover {
background-color: #1e1e1e;
}
.Row-validator {
display: block;
width: 16px;
height: 16px;
cursor: pointer;
}
.Row-validator:hover {
transform: scale(2);
}
+49 -66
View File
@@ -2,7 +2,28 @@ import * as React from 'react';
import { Types, Maybe } from '../../common';
import { Node } from '../../state';
import { Persistent, PersistentSet } from '../../persist';
import { HeaderCell, Column } from './';
import {
Column,
NameColumn,
ValidatorColumn,
LocationColumn,
ImplementationColumn,
NetworkIdColumn,
PeersColumn,
TxsColumn,
UploadColumn,
DownloadColumn,
StateCacheColumn,
BlockNumberColumn,
BlockHashColumn,
FinalizedBlockColumn,
FinalizedHashColumn,
BlockTimeColumn,
BlockPropagationColumn,
LastBlockColumn,
UptimeColumn,
NetworkStateColumn,
} from './';
import './Row.css';
@@ -25,75 +46,41 @@ interface HeaderProps {
export class Row extends React.Component<Row.Props, Row.State> {
public static readonly columns: Column[] = [
Column.NAME,
Column.VALIDATOR,
Column.LOCATION,
Column.IMPLEMENTATION,
Column.NETWORK_ID,
Column.PEERS,
Column.TXS,
Column.UPLOAD,
Column.DOWNLOAD,
Column.STATE_CACHE,
Column.BLOCK_NUMBER,
Column.BLOCK_HASH,
Column.FINALIZED,
Column.FINALIZED_HASH,
Column.BLOCK_TIME,
Column.BLOCK_PROPAGATION,
Column.BLOCK_LAST_TIME,
Column.UPTIME,
Column.NETWORK_STATE,
NameColumn,
ValidatorColumn,
LocationColumn,
ImplementationColumn,
NetworkIdColumn,
PeersColumn,
TxsColumn,
UploadColumn,
DownloadColumn,
StateCacheColumn,
BlockNumberColumn,
BlockHashColumn,
FinalizedBlockColumn,
FinalizedHashColumn,
BlockTimeColumn,
BlockPropagationColumn,
LastBlockColumn,
UptimeColumn,
NetworkStateColumn,
];
public static HEADER = (props: HeaderProps) => {
const { columns, sortBy } = props;
const last = columns.length - 1;
private renderedChangeRef = 0;
return (
<thead>
<tr className="Row-Header">
{columns.map((col, index) => (
<HeaderCell
key={index}
column={col}
index={index}
last={last}
sortBy={sortBy}
/>
))}
</tr>
</thead>
);
};
public state = { update: 0 };
public componentDidMount() {
const { node } = this.props;
node.subscribe(this.onUpdate);
}
public componentWillUnmount() {
const { node } = this.props;
node.unsubscribe(this.onUpdate);
}
public shouldComponentUpdate(
nextProps: Row.Props,
nextState: Row.State
): boolean {
public shouldComponentUpdate(nextProps: Row.Props): boolean {
return (
this.props.node.id !== nextProps.node.id ||
this.state.update !== nextState.update
this.renderedChangeRef !== nextProps.node.changeRef
);
}
public render() {
const { node, columns } = this.props;
this.renderedChangeRef = node.changeRef;
let className = 'Row';
if (node.propagationTime != null) {
@@ -110,9 +97,9 @@ export class Row extends React.Component<Row.Props, Row.State> {
return (
<tr className={className} onClick={this.toggle}>
{columns.map(({ render }, index) => (
<td key={index}>{render(node)}</td>
))}
{columns.map((col, index) =>
React.createElement(col, { node, key: index })
)}
</tr>
);
}
@@ -126,8 +113,4 @@ export class Row extends React.Component<Row.Props, Row.State> {
pins.add(node.name);
}
};
private onUpdate = () => {
this.setState({ update: this.state.update + 1 });
};
}
+24
View File
@@ -0,0 +1,24 @@
.THead {
background: #393838;
}
.THeadCell {
text-align: left;
padding: 6px 13px;
height: 23px;
}
.THeadCell-sortable {
cursor: pointer;
}
.THeadCell-sorted {
cursor: pointer;
background: #e6007a;
color: #fff;
}
.THeadCell-container {
position: relative;
display: inline-block;
}
+50
View File
@@ -0,0 +1,50 @@
import * as React from 'react';
import { Maybe } from '../../common';
import { Column, THeadCell } from './';
import { Persistent } from '../../persist';
import './THead.css';
export namespace THead {
export interface Props {
columns: Column[];
sortBy: Persistent<Maybe<number>>;
}
}
export class THead extends React.Component<THead.Props, {}> {
private sortBy: Maybe<number>;
constructor(props: THead.Props) {
super(props);
this.sortBy = props.sortBy.get();
}
public shouldComponentUpdate(nextProps: THead.Props) {
return this.sortBy !== nextProps.sortBy.get();
}
public render() {
const { columns, sortBy } = this.props;
const last = columns.length - 1;
this.sortBy = sortBy.get();
return (
<thead>
<tr className="THead">
{columns.map((col, index) => (
<THeadCell
key={index}
column={col}
index={index}
last={last}
sortBy={sortBy}
/>
))}
</tr>
</thead>
);
}
}
@@ -7,7 +7,7 @@ import { Persistent } from '../../persist';
import sortAscIcon from '../../icons/triangle-up.svg';
import sortDescIcon from '../../icons/triangle-down.svg';
export namespace HeaderCell {
export namespace THeadCell {
export interface Props {
column: Column;
index: number;
@@ -16,7 +16,7 @@ export namespace HeaderCell {
}
}
export class HeaderCell extends React.Component<HeaderCell.Props, {}> {
export class THeadCell extends React.Component<THeadCell.Props, {}> {
public render() {
const { column, index, last } = this.props;
const { icon, width, label } = column;
@@ -25,10 +25,10 @@ export class HeaderCell extends React.Component<HeaderCell.Props, {}> {
const sortBy = this.props.sortBy.get();
const className =
column.sortBy == null
? ''
? 'THeadCell'
: sortBy === index || sortBy === ~index
? 'HeaderCell-sorted'
: 'HeaderCell-sortable';
? 'THeadCell THeadCell-sorted'
: 'THeadCell THeadCell-sortable';
const i =
sortBy === index ? sortAscIcon : sortBy === ~index ? sortDescIcon : icon;
@@ -38,9 +38,10 @@ export class HeaderCell extends React.Component<HeaderCell.Props, {}> {
style={width ? { width } : undefined}
onClick={this.toggleSort}
>
<Tooltip text={label} inline={true} position={position}>
<span className="THeadCell-container">
<Tooltip text={label} position={position} />
<Icon src={i} />
</Tooltip>
</span>
</th>
);
}
-38
View File
@@ -1,38 +0,0 @@
import * as React from 'react';
import { Tooltip } from '../';
export namespace Truncate {
export interface Props {
text: string;
copy?: boolean;
position?: Tooltip.Props['position'];
}
}
export class Truncate extends React.Component<Truncate.Props, {}> {
public render() {
const { text, position, copy } = this.props;
if (!text) {
return '-';
}
return (
<Tooltip
text={text}
position={position}
copy={copy}
className="Row-Tooltip"
>
<div className="Row-truncate">{text}</div>
</Tooltip>
);
}
public shouldComponentUpdate(nextProps: Truncate.Props): boolean {
return (
this.props.text !== nextProps.text ||
this.props.position !== nextProps.position
);
}
}
+2 -2
View File
@@ -1,5 +1,5 @@
export * from './Column';
export * from './List';
export * from './Truncate';
export * from './Row';
export * from './HeaderCell';
export * from './THeadCell';
export * from './THead';
+17 -17
View File
@@ -97,7 +97,7 @@ export class Location extends React.Component<Location.Props, Location.State> {
validatorRow = (
<tr>
<td>
<Icon src={nodeValidatorIcon} alt="Node" />
<Icon src={nodeValidatorIcon} />
</td>
<td colSpan={5}>
{trimHash(validator, 30)}
@@ -113,53 +113,53 @@ export class Location extends React.Component<Location.Props, Location.State> {
<table className="Location-details Location-details">
<tbody>
<tr>
<td>
<Icon src={nodeIcon} alt="Node" />
<td title="Node">
<Icon src={nodeIcon} />
</td>
<td colSpan={5}>{name}</td>
</tr>
{validatorRow}
<tr>
<td>
<Icon src={nodeTypeIcon} alt="Implementation" />
<td title="Implementation">
<Icon src={nodeTypeIcon} />
</td>
<td colSpan={5}>
{implementation} v{version}
</td>
</tr>
<tr>
<td>
<Icon src={nodeLocationIcon} alt="Location" />
<td title="Location">
<Icon src={nodeLocationIcon} />
</td>
<td colSpan={5}>{city}</td>
</tr>
<tr>
<td>
<Icon src={blockIcon} alt="Block" />
<td title="Block">
<Icon src={blockIcon} />
</td>
<td colSpan={5}>#{formatNumber(height)}</td>
</tr>
<tr>
<td>
<Icon src={blockHashIcon} alt="Block Hash" />
<td title="Block Hash">
<Icon src={blockHashIcon} />
</td>
<td colSpan={5}>{trimHash(hash, 20)}</td>
</tr>
<tr>
<td>
<Icon src={blockTimeIcon} alt="Block Time" />
<td title="Block Time">
<Icon src={blockTimeIcon} />
</td>
<td style={{ width: 80 }}>
{secondsWithPrecision(blockTime / 1000)}
</td>
<td>
<Icon src={propagationTimeIcon} alt="Block Propagation Time" />
<td title="Block Propagation Time">
<Icon src={propagationTimeIcon} />
</td>
<td style={{ width: 58 }}>
{propagationTime == null ? '∞' : milliOrSecond(propagationTime)}
</td>
<td>
<Icon src={lastTimeIcon} alt="Last Block Time" />
<td title="Last Block Time">
<Icon src={lastTimeIcon} />
</td>
<td style={{ minWidth: 82 }}>
<Ago when={blockTimestamp} />
+1 -1
View File
@@ -34,7 +34,7 @@ export class Map extends React.Component<Map.Props, Map.State> {
left: 0,
};
public componentWillMount() {
public componentDidMount() {
this.onResize();
window.addEventListener('resize', this.onResize);
+7 -4
View File
@@ -19,14 +19,17 @@ export function OfflineIndicator(
return null;
case 'offline':
return (
<div className="OfflineIndicator">
<Icon src={offlineIcon} alt="Offline" />
<div className="OfflineIndicator" title="Offline">
<Icon src={offlineIcon} />
</div>
);
case 'upgrade-requested':
return (
<div className="OfflineIndicator OfflineIndicator-upgrade">
<Icon src={upgradeIcon} alt="New Version Available" />
<div
className="OfflineIndicator OfflineIndicator-upgrade"
title="New Version Available"
>
<Icon src={upgradeIcon} />
</div>
);
}
+52 -22
View File
@@ -8,6 +8,7 @@
// https://github.com/paritytech/oo7/blob/251ba2b7c45503b68eab4320c270b5afa9bccb60/packages/polkadot-identicon/src/index.jsx
import * as React from 'react';
import { blake2AsU8a, decodeAddress } from '@polkadot/util-crypto';
import { getSVGShadowRoot, W3SVG } from '../utils';
interface Circle {
cx: number;
@@ -172,20 +173,19 @@ function getColors(address: string): string[] {
/**
* @description Generate a array of the circles that make up an indenticon
*/
export default function generate(
address: string,
isSixPoint = false
): Circle[] {
function generate(address: string, isSixPoint = false): Circle[] {
const colors = getColors(address);
return [OUTER_CIRCLE].concat(
getCircleXY(isSixPoint).map(
([cx, cy], index): Circle => ({
cx,
cy,
r: Z,
fill: colors[index],
})
([cx, cy], index): Circle => {
return {
cx,
cy,
r: Z,
fill: colors[index],
};
}
)
);
}
@@ -194,24 +194,54 @@ export namespace PolkadotIcon {
export interface Props {
account: string;
size: number;
className?: string;
}
}
const rendered = new Set<string>();
// Lazily render the icon in the DOM, so that we can referenced
// it by id using shadow DOM.
function renderShadowIcon(account: string) {
if (!rendered.has(account)) {
rendered.add(account);
const symEl = document.createElementNS(W3SVG, 'symbol');
symEl.setAttribute('id', account);
symEl.setAttribute('viewBox', '0 0 64 64');
generate(account, false).forEach(({ cx, cy, r, fill }) => {
const circle = document.createElementNS(W3SVG, 'circle');
circle.setAttribute('cx', String(cx));
circle.setAttribute('cy', String(cy));
circle.setAttribute('r', String(r));
circle.setAttribute('fill', fill);
symEl.appendChild(circle);
});
getSVGShadowRoot().appendChild(symEl);
}
}
export class PolkadotIcon extends React.Component<PolkadotIcon.Props, {}> {
public render(): React.ReactNode {
const { account, size } = this.props;
public shouldComponentUpdate(nextProps: PolkadotIcon.Props) {
return (
<svg width={size} height={size} viewBox="0 0 64 64">
{generate(account, false).map(this.renderCircle)}
</svg>
this.props.account !== nextProps.account ||
this.props.size !== nextProps.size
);
}
private renderCircle = (
{ cx, cy, r, fill }: Circle,
key: number
): React.ReactNode => {
return <circle key={key} cx={cx} cy={cy} r={r} fill={fill} />;
};
public render(): React.ReactNode {
const { account, size, className } = this.props;
renderShadowIcon(account);
return (
<svg width={size} height={size} className={className}>
<use href={`#${account}`} />
</svg>
);
}
}
+1 -1
View File
@@ -23,7 +23,7 @@ export class Setting extends React.Component<Setting.Props, {}> {
return (
<div className={className} onClick={this.toggle}>
<Icon src={icon} alt={label} />
<Icon src={icon} />
{label}
<span className="Setting-switch">
<span className="Setting-knob" />
+12
View File
@@ -4,3 +4,15 @@
stroke: currentcolor;
margin: 0 -1px -3px -1px;
}
.Sparkline path {
pointer-events: none;
}
.Sparkline .Sparkline-cursor {
display: none;
}
.Sparkline:hover .Sparkline-cursor {
display: initial;
}
+61 -49
View File
@@ -1,6 +1,5 @@
import * as React from 'react';
import { Types, Maybe } from '../common';
import sparkline from '@fnando/sparkline';
import { Tooltip } from './';
import './Sparkline.css';
@@ -18,60 +17,64 @@ export namespace Sparkline {
}
export class Sparkline extends React.Component<Sparkline.Props, {}> {
private el: SVGSVGElement;
private cursor: SVGPathElement;
private update: Tooltip.UpdateCallback;
public componentDidMount() {
sparkline(this.el, this.props.values, {
spotRadius: 0.1,
minScale: this.props.minScale,
interactive: true,
onmousemove: this.onMouseMove,
});
}
public shouldComponentUpdate(nextProps: Sparkline.Props): boolean {
const { stroke, width, height, minScale, format } = this.props;
const { stroke, width, height, minScale, format, values } = this.props;
if (
return (
values !== nextProps.values ||
stroke !== nextProps.stroke ||
width !== nextProps.width ||
height !== nextProps.height ||
format !== nextProps.format
) {
return true;
}
if (this.props.values !== nextProps.values) {
sparkline(this.el, nextProps.values, {
spotRadius: 0.1,
minScale,
interactive: true,
onmousemove: this.onMouseMove,
});
}
return false;
}
public render() {
const { stroke, width, height } = this.props;
return (
<Tooltip text="-" onInit={this.onTooltipInit}>
<svg
className="Sparkline"
ref={this.onRef}
width={width}
height={height}
strokeWidth={stroke}
/>
</Tooltip>
);
}
private onRef = (el: SVGSVGElement) => {
this.el = el;
public render() {
const { stroke, width, height, minScale, values } = this.props;
const padding = stroke / 2;
const paddedHeight = height - padding;
const paddedWidth = width - 2;
const max = Math.max(minScale || 0, ...values);
const offset = paddedWidth / (values.length - 1);
let path = '';
values.forEach((value, index) => {
const x = 1 + index * offset;
const y = padding + (1 - value / max) * paddedHeight;
if (path) {
path += ` L ${x} ${y}`;
} else {
path = `${x} ${y}`;
}
});
return (
<>
<Tooltip text="-" onInit={this.onTooltipInit} />
<svg
className="Sparkline"
width={width}
height={height}
strokeWidth={stroke}
onMouseMove={this.onMouseMove}
onMouseLeave={this.onMouseLeave}
>
<path d={`M 0 ${height} L ${path} V ${height} Z`} stroke="none" />
<path d={`M ${path}`} fill="none" />
<path className="Sparkline-cursor" strokeWidth="2" ref={this.onRef} />
</svg>
</>
);
}
private onRef = (cursor: SVGPathElement) => {
this.cursor = cursor;
};
private onTooltipInit = (update: Tooltip.UpdateCallback) => {
@@ -79,13 +82,22 @@ export class Sparkline extends React.Component<Sparkline.Props, {}> {
};
private onMouseMove = (
_event: MouseEvent,
data: { value: number; index: number }
event: React.MouseEvent<SVGSVGElement, MouseEvent>
) => {
const { format, stamps } = this.props;
const { width, height, values, format, stamps } = this.props;
const offset = (width - 2) / (values.length - 1);
const cur =
Math.round((event.nativeEvent.offsetX - 1 - offset / 2) / offset) | 0;
this.cursor.setAttribute('d', `M ${1 + offset * cur} 0 V ${height}`);
const str = format
? format(data.value, stamps ? stamps[data.index] : null)
: `${data.value}`;
? format(values[cur], stamps ? stamps[cur] : null)
: `${values[cur]}`;
this.update(str);
};
private onMouseLeave = () => {
this.cursor.removeAttribute('d');
};
}
+1 -1
View File
@@ -13,7 +13,7 @@ export namespace Tile {
export function Tile(props: Tile.Props) {
return (
<div className="Tile">
<Icon src={props.icon} alt={props.title} />
<Icon src={props.icon} />
<span className="Tile-label">{props.title}</span>
<span className="Tile-content">{props.children}</span>
</div>
+1 -11
View File
@@ -74,17 +74,7 @@
right: 0;
}
.Tooltip-container:hover .Tooltip {
:hover > .Tooltip {
display: block;
animation: show 0.15s forwards;
}
@keyframes show {
from {
opacity: 0;
}
to {
opacity: 1;
}
}
+31 -27
View File
@@ -1,12 +1,12 @@
import * as React from 'react';
import { Maybe } from '../common';
import './Tooltip.css';
export namespace Tooltip {
export interface Props {
text: string;
copy?: boolean;
inline?: boolean;
copy?: (cb: CopyCallback) => void;
className?: string;
position?: 'left' | 'right' | 'center';
onInit?: (update: UpdateCallback) => void;
@@ -17,6 +17,7 @@ export namespace Tooltip {
}
export type UpdateCallback = (text: string) => void;
export type CopyCallback = Maybe<() => void>;
}
function copyToClipboard(text: string) {
@@ -32,33 +33,42 @@ export class Tooltip extends React.Component<Tooltip.Props, Tooltip.State> {
public state = { copied: false };
private el: HTMLDivElement;
private timer: NodeJS.Timer;
private timer: NodeJS.Timer | null = null;
public componentDidMount() {
if (this.props.onInit) {
this.props.onInit(this.update);
}
if (this.props.copy) {
this.props.copy(this.onClick);
}
}
public componentWillUnmount() {
clearTimeout(this.timer);
if (this.timer) {
clearTimeout(this.timer);
}
if (this.props.copy) {
this.props.copy(null);
}
}
public shouldComponentUpdate(
nextProps: Tooltip.Props,
nextState: Tooltip.State
) {
return (
this.props.text !== nextProps.text ||
this.state.copied !== nextState.copied
);
}
public render() {
const { text, inline, className, position } = this.props;
const { text, className, position } = this.props;
const { copied } = this.state;
let containerClass = 'Tooltip-container';
let tooltipClass = 'Tooltip';
if (className) {
containerClass += ' ' + className;
}
if (inline) {
containerClass += ' Tooltip-container-inline';
}
if (position && position !== 'center') {
tooltipClass += ` Tooltip-${position}`;
}
@@ -68,11 +78,8 @@ export class Tooltip extends React.Component<Tooltip.Props, Tooltip.State> {
}
return (
<div className={containerClass} onClick={this.onClick}>
<div className={tooltipClass} ref={this.onRef}>
{copied ? 'Copied to clipboard!' : text}
</div>
{this.props.children}
<div className={tooltipClass} ref={this.onRef}>
{copied ? 'Copied to clipboard!' : text}
</div>
);
}
@@ -85,16 +92,12 @@ export class Tooltip extends React.Component<Tooltip.Props, Tooltip.State> {
this.el.textContent = text;
};
private onClick = (event: React.MouseEvent<HTMLDivElement>) => {
if (this.props.copy !== true) {
return;
}
private onClick = () => {
copyToClipboard(this.props.text);
event.stopPropagation();
clearTimeout(this.timer);
if (this.timer) {
clearTimeout(this.timer);
}
this.setState({ copied: true });
this.timer = setTimeout(this.restore, 2000);
@@ -102,5 +105,6 @@ export class Tooltip extends React.Component<Tooltip.Props, Tooltip.State> {
private restore = () => {
this.setState({ copied: false });
this.timer = null;
};
}
+32
View File
@@ -0,0 +1,32 @@
import * as React from 'react';
export namespace Truncate {
export interface Props {
text: string;
chars?: number;
}
}
export class Truncate extends React.Component<Truncate.Props, {}> {
public shouldComponentUpdate(nextProps: Truncate.Props): boolean {
return this.props.text !== nextProps.text;
}
public render() {
const { text, chars } = this.props;
if (!text) {
return '-';
}
if (chars != null && text.length <= chars) {
return text;
}
return chars ? (
`${text.substr(0, chars - 1)}`
) : (
<div className="Column-truncate">{text}</div>
);
}
}
+1
View File
@@ -10,6 +10,7 @@ export * from './Tile';
export * from './Ago';
export * from './OfflineIndicator';
export * from './Sparkline';
export * from './Truncate';
export * from './Tooltip';
export * from './Filter';
export * from './PolkadotIcon';
File diff suppressed because one or more lines are too long

Before

Width:  |  Height:  |  Size: 5.6 KiB

After

Width:  |  Height:  |  Size: 5.5 KiB

+3 -6
View File
@@ -1,9 +1,6 @@
<?xml version="1.0" encoding="UTF-8"?>
<svg width="512px" height="512px" viewBox="0 0 512 512" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
<!-- Generator: Sketch 64 (93537) - https://sketch.com -->
<title>Darwinia</title>
<desc>Darwinia Network Logo</desc>
<g id="Darwinia" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd">
<path d="M113.288481,145 C128.136677,145 140.174312,157.023943 140.174312,171.856735 C140.174312,183.469807 132.793886,193.362319 122.463232,197.108867 L122.464671,336.232914 C141.696857,335.384773 159.580699,331.869555 175.47006,326.043141 L175.46949,219.38341 C165.138836,215.636862 157.75841,205.744351 157.75841,194.131279 C157.75841,179.298487 169.796045,167.274543 184.644241,167.274543 C199.492436,167.274543 211.530071,179.298487 211.530071,194.131279 C211.530071,205.744351 204.149645,215.636862 193.818991,219.38341 L193.819851,317.813403 C211.486095,308.273137 225.639172,295.360902 235.02445,279.767728 C228.656572,274.035269 224.654434,265.734349 224.654434,256.5 C224.654434,239.206326 238.688237,225.188356 256,225.188356 C257.421586,225.188356 258.821069,225.282882 260.192492,225.465984 C284.066647,183.99976 335.257967,158.88464 396.335714,158.249774 L398.711519,158.237443 L496.825688,158.237443 C501.892521,158.237443 506,162.340477 506,167.401826 C506,172.282413 502.180673,176.271914 497.364749,176.550653 L496.825688,176.56621 L479.24159,176.56621 L479.242252,315.890998 C489.573026,319.637265 496.953109,329.529235 496.953109,341.143265 C496.953109,355.974833 484.914915,368 470.067278,368 C455.219642,368 443.181448,355.974833 443.181448,341.143265 C443.181448,329.528083 450.562994,319.635304 460.895378,315.889884 L460.892966,176.56621 L407.883792,176.56621 L407.886493,315.890998 C418.217267,319.637265 425.59735,329.529235 425.59735,341.143265 C425.59735,355.974833 413.559155,368 398.711519,368 C383.863882,368 371.825688,355.974833 371.825688,341.143265 C371.825688,329.529235 379.205771,319.637265 389.536545,315.890998 L389.535329,176.767086 C370.303143,177.615227 352.419301,181.130445 336.52994,186.956859 L336.530733,293.616455 C346.861507,297.362722 354.24159,307.254692 354.24159,318.868721 C354.24159,333.70029 342.203396,345.725457 327.355759,345.725457 C312.508123,345.725457 300.469929,333.70029 300.469929,318.868721 C300.469929,307.254692 307.850012,297.362722 318.180786,293.616455 L318.180149,195.186597 C300.513905,204.726863 286.360828,217.639098 276.97555,233.232272 C283.34359,238.964127 287.345566,247.265003 287.345566,256.5 C287.345566,273.79246 273.311194,287.811644 256,287.811644 C254.57846,287.811644 253.179017,287.71711 251.807626,287.533993 C227.933663,329.000088 176.742218,354.115358 115.664286,354.750226 L113.288481,354.762557 L15.1743119,354.762557 C10.1074794,354.762557 6,350.659523 6,345.598174 C6,340.717587 9.81932711,336.728086 14.6352514,336.449347 L15.1743119,336.43379 L32.7584098,336.432772 L32.7584368,197.109036 C22.4275387,193.362616 15.0468909,183.469982 15.0468909,171.856735 C15.0468909,157.023943 27.0845259,145 41.9327217,145 C56.7809176,145 68.8185525,157.023943 68.8185525,171.856735 C68.8185525,183.468835 61.439362,193.360663 51.1100663,197.107926 L51.1070336,336.432772 L104.11315,336.432772 L104.113731,197.108867 C93.7830763,193.362319 86.4026504,183.469807 86.4026504,171.856735 C86.4026504,157.023943 98.4402853,145 113.288481,145 Z" id="Darwinia" fill="currentColor"></path>
<svg width="512" height="512" viewBox="0 0 512 512" version="1.1" xmlns="http://www.w3.org/2000/svg">
<g stroke="none" stroke-width="1" fill="none" fill-rule="evenodd">
<path d="M113.288481,145 C128.136677,145 140.174312,157.023943 140.174312,171.856735 C140.174312,183.469807 132.793886,193.362319 122.463232,197.108867 L122.464671,336.232914 C141.696857,335.384773 159.580699,331.869555 175.47006,326.043141 L175.46949,219.38341 C165.138836,215.636862 157.75841,205.744351 157.75841,194.131279 C157.75841,179.298487 169.796045,167.274543 184.644241,167.274543 C199.492436,167.274543 211.530071,179.298487 211.530071,194.131279 C211.530071,205.744351 204.149645,215.636862 193.818991,219.38341 L193.819851,317.813403 C211.486095,308.273137 225.639172,295.360902 235.02445,279.767728 C228.656572,274.035269 224.654434,265.734349 224.654434,256.5 C224.654434,239.206326 238.688237,225.188356 256,225.188356 C257.421586,225.188356 258.821069,225.282882 260.192492,225.465984 C284.066647,183.99976 335.257967,158.88464 396.335714,158.249774 L398.711519,158.237443 L496.825688,158.237443 C501.892521,158.237443 506,162.340477 506,167.401826 C506,172.282413 502.180673,176.271914 497.364749,176.550653 L496.825688,176.56621 L479.24159,176.56621 L479.242252,315.890998 C489.573026,319.637265 496.953109,329.529235 496.953109,341.143265 C496.953109,355.974833 484.914915,368 470.067278,368 C455.219642,368 443.181448,355.974833 443.181448,341.143265 C443.181448,329.528083 450.562994,319.635304 460.895378,315.889884 L460.892966,176.56621 L407.883792,176.56621 L407.886493,315.890998 C418.217267,319.637265 425.59735,329.529235 425.59735,341.143265 C425.59735,355.974833 413.559155,368 398.711519,368 C383.863882,368 371.825688,355.974833 371.825688,341.143265 C371.825688,329.529235 379.205771,319.637265 389.536545,315.890998 L389.535329,176.767086 C370.303143,177.615227 352.419301,181.130445 336.52994,186.956859 L336.530733,293.616455 C346.861507,297.362722 354.24159,307.254692 354.24159,318.868721 C354.24159,333.70029 342.203396,345.725457 327.355759,345.725457 C312.508123,345.725457 300.469929,333.70029 300.469929,318.868721 C300.469929,307.254692 307.850012,297.362722 318.180786,293.616455 L318.180149,195.186597 C300.513905,204.726863 286.360828,217.639098 276.97555,233.232272 C283.34359,238.964127 287.345566,247.265003 287.345566,256.5 C287.345566,273.79246 273.311194,287.811644 256,287.811644 C254.57846,287.811644 253.179017,287.71711 251.807626,287.533993 C227.933663,329.000088 176.742218,354.115358 115.664286,354.750226 L113.288481,354.762557 L15.1743119,354.762557 C10.1074794,354.762557 6,350.659523 6,345.598174 C6,340.717587 9.81932711,336.728086 14.6352514,336.449347 L15.1743119,336.43379 L32.7584098,336.432772 L32.7584368,197.109036 C22.4275387,193.362616 15.0468909,183.469982 15.0468909,171.856735 C15.0468909,157.023943 27.0845259,145 41.9327217,145 C56.7809176,145 68.8185525,157.023943 68.8185525,171.856735 C68.8185525,183.468835 61.439362,193.360663 51.1100663,197.107926 L51.1070336,336.432772 L104.11315,336.432772 L104.113731,197.108867 C93.7830763,193.362319 86.4026504,183.469807 86.4026504,171.856735 C86.4026504,157.023943 98.4402853,145 113.288481,145 Z" fill="currentColor"></path>
</g>
</svg>

Before

Width:  |  Height:  |  Size: 3.4 KiB

After

Width:  |  Height:  |  Size: 3.2 KiB

+6 -6
View File
@@ -1,10 +1,10 @@
<?xml version="1.0" encoding="UTF-8"?>
<svg width="200px" height="200px" viewBox="0 0 200 200" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
<g id="Artboard" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd" stroke-linecap="square">
<g id="Group" transform="translate(48.000000, 40.000000)" stroke="currentColor" stroke-width="22">
<path d="M52.5,0.5 L52.5,120.5" id="Line"></path>
<path d="M52.5,0.5 L52.5,120.5" id="Line" transform="translate(52.500000, 60.500000) rotate(60.000000) translate(-52.500000, -60.500000) "></path>
<path d="M52.5,0.5 L52.5,120.5" id="Line" transform="translate(52.500000, 60.500000) rotate(-60.000000) translate(-52.500000, -60.500000) "></path>
<svg width="200" height="200" viewBox="0 0 200 200" version="1.1" xmlns="http://www.w3.org/2000/svg">
<g stroke="none" stroke-width="1" fill="none" fill-rule="evenodd" stroke-linecap="square">
<g transform="translate(48.000000, 40.000000)" stroke="currentColor" stroke-width="22">
<path d="M52.5,0.5 L52.5,120.5"></path>
<path d="M52.5,0.5 L52.5,120.5" transform="translate(52.500000, 60.500000) rotate(60.000000) translate(-52.500000, -60.500000) "></path>
<path d="M52.5,0.5 L52.5,120.5" transform="translate(52.500000, 60.500000) rotate(-60.000000) translate(-52.500000, -60.500000) "></path>
</g>
</g>
</svg>

Before

Width:  |  Height:  |  Size: 814 B

After

Width:  |  Height:  |  Size: 712 B

+1 -1
View File
@@ -1 +1 @@
<?xml version="1.0" encoding="iso-8859-1" standalone="yes"?><svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" version="1.1" id="polkadot-js" x="0" y="0" viewBox="0 0 512 512" style="enable-background:new 0 0 512 512;zoom: 1;" xml:space="preserve"><circle style="fill:currentColor;opacity:0.25" cx="256" cy="256" r="200"></circle><rect stroke="currentColor" height="400" x="80" stroke-width="0" width="400" y="48" fill="none" transform=""></rect><path style="fill:currentColor;" d="M391.05,232.97 c-7.125,-20.664 -36.341,-20.664 -42.04,-3.563 c-5.7,17.101 13.539,27.076 43.466,37.053 c29.927,9.976 48.454,36.341 44.891,59.854 s-13.539,54.154 -72.68,54.154 c-36.963,0 -57.505,-23.103 -67.714,-40.429 l33.511,-20.138 c0,0 11.401,24.227 32.777,24.227 c21.377,0 29.927,-7.125 29.927,-23.514 c0,-19.951 -69.118,-27.076 -79.806,-61.279 s3.563,-82.656 53.441,-79.094 c31.175,2.227 48.71,17.535 57.478,28.703 L391.05,232.97 z " visibility="visible"></path><path style="fill:currentColor;" d="M235.714,183.805 c0,0 0,120.421 0,136.81 c0,16.388 -14.251,24.94 -28.502,22.089 c-14.251,-2.85 -20.664,-19.951 -20.664,-19.951 l-32.777,22.089 c0,0 7.125,32.777 52.016,35.628 c44.891,2.85 70.542,-24.227 70.542,-47.029 s0,-149.636 0,-149.636 L235.714,183.805 L235.714,183.805 z " visibility="visible"></path><circle style="fill:currentColor;" cx="90" cy="180" r="64"></circle></svg>
<?xml version="1.0" encoding="iso-8859-1" standalone="yes"?><svg xmlns="http://www.w3.org/2000/svg" x="0" y="0" viewBox="0 0 512 512"><circle style="fill:currentColor;opacity:0.25" cx="256" cy="256" r="200"></circle><rect stroke="currentColor" height="400" x="80" stroke-width="0" width="400" y="48" fill="none" transform=""></rect><path style="fill:currentColor;" d="M391.05,232.97 c-7.125,-20.664 -36.341,-20.664 -42.04,-3.563 c-5.7,17.101 13.539,27.076 43.466,37.053 c29.927,9.976 48.454,36.341 44.891,59.854 s-13.539,54.154 -72.68,54.154 c-36.963,0 -57.505,-23.103 -67.714,-40.429 l33.511,-20.138 c0,0 11.401,24.227 32.777,24.227 c21.377,0 29.927,-7.125 29.927,-23.514 c0,-19.951 -69.118,-27.076 -79.806,-61.279 s3.563,-82.656 53.441,-79.094 c31.175,2.227 48.71,17.535 57.478,28.703 L391.05,232.97 z " visibility="visible"></path><path style="fill:currentColor;" d="M235.714,183.805 c0,0 0,120.421 0,136.81 c0,16.388 -14.251,24.94 -28.502,22.089 c-14.251,-2.85 -20.664,-19.951 -20.664,-19.951 l-32.777,22.089 c0,0 7.125,32.777 52.016,35.628 c44.891,2.85 70.542,-24.227 70.542,-47.029 s0,-149.636 0,-149.636 L235.714,183.805 L235.714,183.805 z " visibility="visible"></path><circle style="fill:currentColor;" cx="90" cy="180" r="64"></circle></svg>

Before

Width:  |  Height:  |  Size: 1.4 KiB

After

Width:  |  Height:  |  Size: 1.2 KiB

+27 -22
View File
@@ -1,3 +1,4 @@
import * as React from 'react';
import { Types, Maybe, SortedCollection } from './common';
import { Column } from './components/List';
@@ -69,7 +70,7 @@ export class Node {
public lon: Maybe<Types.Longitude>;
public city: Maybe<Types.City>;
private readonly subscriptions = new Set<(node: Node) => void>();
private _changeRef = 0;
private readonly subscriptionsConsensus = new Set<(node: Node) => void>();
constructor(
@@ -188,29 +189,36 @@ export class Node {
}
}
public subscribe(handler: (node: Node) => void) {
this.subscriptions.add(handler);
}
public unsubscribe(handler: (node: Node) => void) {
this.subscriptions.delete(handler);
}
public subscribeConsensus(handler: (node: Node) => void) {
this.subscriptionsConsensus.add(handler);
}
public unsubscribeConsensus(handler: (node: Node) => void) {
this.subscriptionsConsensus.delete(handler);
public get changeRef(): number {
return this._changeRef;
}
private trigger() {
for (const handler of this.subscriptions.values()) {
handler(this);
}
this._changeRef += 1;
}
}
export function bindState(bind: React.Component, state: State): Update {
let isUpdating = false;
return (changes) => {
// Apply new changes to the state immediately
Object.assign(state, changes);
// Trigger React update on next animation frame only once
if (!isUpdating) {
isUpdating = true;
window.requestAnimationFrame(() => {
bind.forceUpdate();
isUpdating = false;
});
}
return state;
};
}
export namespace State {
export interface Settings {
location: boolean;
@@ -262,11 +270,8 @@ export interface State {
}
export type Update = <K extends keyof State>(
changes: Pick<State, K> | null
changes: Pick<State, K>
) => Readonly<State>;
export type UpdateBound = <K extends keyof State>(
changes: Pick<State, K> | null
) => void;
export interface ChainData {
label: Types.ChainLabel;
+16
View File
@@ -95,3 +95,19 @@ export function setHashData(val: HashData) {
window.location.hash = `#${tab}/${encodeURIComponent(chain)}`;
}
let root: null | SVGSVGElement = null;
export const W3SVG = 'http://www.w3.org/2000/svg';
// Get a root node where we all SVG symbols can be stored
// see: Icon.tsx
export function getSVGShadowRoot(): SVGSVGElement {
if (!root) {
root = document.createElementNS(W3SVG, 'svg');
root.setAttribute('style', 'display: none;');
document.body.appendChild(root);
}
return root;
}
+13 -38
View File
@@ -184,10 +184,6 @@
lodash "^4.17.13"
to-fast-properties "^2.0.0"
"@fnando/sparkline@maciejhirsz/sparkline":
version "0.3.10"
resolved "https://codeload.github.com/maciejhirsz/sparkline/tar.gz/2bdb002b171436be078a84f1e4e617a44ef1fb42"
"@istanbuljs/load-nyc-config@^1.0.0":
version "1.0.0"
resolved "https://registry.npmjs.org/@istanbuljs/load-nyc-config/-/load-nyc-config-1.0.0.tgz#10602de5570baea82f8afbfa2630b24e7a8cfe5b"
@@ -246,11 +242,6 @@
dependencies:
any-observable "^0.3.0"
"@tanem/svg-injector@^1.2.0":
version "1.2.1"
resolved "https://registry.npmjs.org/@tanem/svg-injector/-/svg-injector-1.2.1.tgz#3120e90246d0eb3c4fc6c61586a6f028a3c658ae"
integrity sha512-mA5Q5ulPoGQ+e08Vts1R6xw2QU0BKEnMH/KcqoYoS7Gk6imvMTpyFPeu1g+NOZObSIoAzA3/kRzY8m96cEBA2A==
"@types/bn.js@^4.11.6":
version "4.11.6"
resolved "https://registry.npmjs.org/@types/bn.js/-/bn.js-4.11.6.tgz#c306c70d9358aaea33cd4eda092a742b9505967c"
@@ -302,13 +293,6 @@
dependencies:
"@types/react" "*"
"@types/react-svg@3.0.0":
version "3.0.0"
resolved "https://registry.npmjs.org/@types/react-svg/-/react-svg-3.0.0.tgz#ebbd0a095339ba20d9ba1d8fb3441eef9aeb5d11"
integrity sha512-9KO459enRlNfMWBAQEQraJJb7YeyIZ+U4+R2OVYmtl3WJlN7EKHKHpd9lSxCKzQa7BxQLgUwWxwTa3Nx7GtwEA==
dependencies:
"@types/react" "*"
"@types/react@*", "@types/react@^16.9.34":
version "16.9.34"
resolved "https://registry.npmjs.org/@types/react/-/react-16.9.34.tgz#f7d5e331c468f53affed17a8a4d488cd44ea9349"
@@ -7745,15 +7729,14 @@ react-dev-utils@^5.0.2:
strip-ansi "3.0.1"
text-table "0.2.0"
react-dom@^16.13.1:
version "16.13.1"
resolved "https://registry.npmjs.org/react-dom/-/react-dom-16.13.1.tgz#c1bd37331a0486c078ee54c4740720993b2e0e7f"
integrity sha512-81PIMmVLnCNLO/fFOQxdQkvEq/+Hfpv24XNJfpyZhTRfO0QcmQIF/PgCa1zCOj2w1hrn12MFLyaJ/G0+Mxtfag==
react-dom@^17.0.1:
version "17.0.1"
resolved "https://registry.yarnpkg.com/react-dom/-/react-dom-17.0.1.tgz#1de2560474ec9f0e334285662ede52dbc5426fc6"
integrity sha512-6eV150oJZ9U2t9svnsspTMrWNyHc6chX0KzDeAOXftRa8bNeOKTTfCJ7KorIwenkHd2xqVTBTCZd79yk/lx/Ug==
dependencies:
loose-envify "^1.1.0"
object-assign "^4.1.1"
prop-types "^15.6.2"
scheduler "^0.19.1"
scheduler "^0.20.1"
react-error-overlay@^4.0.1:
version "4.0.1"
@@ -7820,21 +7803,13 @@ react-scripts-ts@3.1.0:
optionalDependencies:
fsevents "^1.1.3"
react-svg@4.1.1:
version "4.1.1"
resolved "https://registry.npmjs.org/react-svg/-/react-svg-4.1.1.tgz#6151831e6f03e1ef5a090c61b12c30aa48185425"
integrity sha512-PAFIRcXnOT2VXcP31DZJ+Xw11NBkvvDAfnAm5C2iVwVbngFUXPbqEKT0V4jquAOGE7ZDF4/Ok0xKInCkSKq1Iw==
dependencies:
"@tanem/svg-injector" "^1.2.0"
react@^16.13.1:
version "16.13.1"
resolved "https://registry.npmjs.org/react/-/react-16.13.1.tgz#2e818822f1a9743122c063d6410d85c1e3afe48e"
integrity sha512-YMZQQq32xHLX0bz5Mnibv1/LHb3Sqzngu7xstSM+vrkE5Kzr9xE0yMByK5kMoTK30YVJE61WfbxIFFvfeDKT1w==
react@^17.0.1:
version "17.0.1"
resolved "https://registry.yarnpkg.com/react/-/react-17.0.1.tgz#6e0600416bd57574e3f86d92edba3d9008726127"
integrity sha512-lG9c9UuMHdcAexXtigOZLX8exLWkW0Ku29qPRU8uhF2R9BN96dLCt0psvzPLlHc5OWkgymP3qwTRgbnw5BKx3w==
dependencies:
loose-envify "^1.1.0"
object-assign "^4.1.1"
prop-types "^15.6.2"
read-pkg-up@^1.0.1:
version "1.0.1"
@@ -8346,10 +8321,10 @@ sax@^1.2.1, sax@^1.2.4, sax@~1.2.1:
resolved "https://registry.npmjs.org/sax/-/sax-1.2.4.tgz#2816234e2378bddc4e5354fab5caa895df7100d9"
integrity sha512-NqVDv9TpANUjFm0N8uM5GxL36UgKi9/atZw+x7YFnQ8ckwFGKrl4xX4yWtrey3UJm5nP1kUbnYgLopqWNSRhWw==
scheduler@^0.19.1:
version "0.19.1"
resolved "https://registry.npmjs.org/scheduler/-/scheduler-0.19.1.tgz#4f3e2ed2c1a7d65681f4c854fa8c5a1ccb40f196"
integrity sha512-n/zwRWRYSUj0/3g/otKDRPMh6qv2SYMWNq85IEa8iZyAv8od9zDYpGSnpBEjNgcMNq6Scbu5KfIPxNF72R/2EA==
scheduler@^0.20.1:
version "0.20.1"
resolved "https://registry.yarnpkg.com/scheduler/-/scheduler-0.20.1.tgz#da0b907e24026b01181ecbc75efdc7f27b5a000c"
integrity sha512-LKTe+2xNJBNxu/QhHvDR14wUXHRQbVY5ZOYpOGWRzhydZUqrLb2JBvLPY7cAqFmqrWuDED0Mjk7013SZiOz6Bw==
dependencies:
loose-envify "^1.1.0"
object-assign "^4.1.1"