Add prettier and format the code (#242)

* prettier

* linter

* add prettier, and format the code

* make the travis pass

Signed-off-by: Daniel Maricic <daniel@woss.io>

* travis to make a build

Signed-off-by: Daniel Maricic <daniel@woss.io>

* tslint and prettier + travis

Signed-off-by: Daniel Maricic <daniel@woss.io>

* useless setting, we use spaces

Signed-off-by: Daniel Maricic <daniel@woss.io>

* backend tests added to the travis

Signed-off-by: Daniel Maricic <daniel@woss.io>
This commit is contained in:
Daniel Maricic
2020-03-31 16:14:48 +02:00
committed by GitHub
parent b9853be186
commit 20a0283380
54 changed files with 2043 additions and 692 deletions
+1 -3
View File
@@ -28,10 +28,8 @@ before_script:
- if [ $TRAVIS_RUST_VERSION = $CLIPPY_TOOLCHAIN ]; then rustup component add clippy-preview --toolchain=$CLIPPY_TOOLCHAIN; fi - if [ $TRAVIS_RUST_VERSION = $CLIPPY_TOOLCHAIN ]; then rustup component add clippy-preview --toolchain=$CLIPPY_TOOLCHAIN; fi
- curl https://rustwasm.github.io/wasm-pack/installer/init.sh -sSf | sh -s -- -f - curl https://rustwasm.github.io/wasm-pack/installer/init.sh -sSf | sh -s -- -f
script: ./test.sh
script: script:
- yarn - yarn
- yarn check:all
- yarn build:all - yarn build:all
- yarn test
- cd backend && cargo test - cd backend && cargo test
+2
View File
@@ -14,6 +14,8 @@
"scripts": { "scripts": {
"deploy": "scripts/deploy.sh", "deploy": "scripts/deploy.sh",
"build:all": "scripts/build-all.sh", "build:all": "scripts/build-all.sh",
"check:frontend": "cd packages/frontend; yarn pretty:check",
"check:all": "yarn check:common && yarn check:frontend",
"start:frontend": "scripts/start-frontend.sh", "start:frontend": "scripts/start-frontend.sh",
"build:frontend": "scripts/build-frontend.sh", "build:frontend": "scripts/build-frontend.sh",
"build:common": "tsc -p packages/common", "build:common": "tsc -p packages/common",
+4
View File
@@ -0,0 +1,4 @@
{
"singleQuote": true,
"semi": true
}
+18 -4
View File
@@ -20,21 +20,27 @@
}, },
"dependencies": { "dependencies": {
"@fnando/sparkline": "maciejhirsz/sparkline", "@fnando/sparkline": "maciejhirsz/sparkline",
"@types/react-measure": "^2.0.5",
"@polkadot/util-crypto": "^1.6.1", "@polkadot/util-crypto": "^1.6.1",
"@types/react-measure": "^2.0.5",
"blakejs": "^1.1.0",
"husky": "^4.2.3",
"lint-staged": "^10.1.0",
"react": "16.4.0", "react": "16.4.0",
"react-dom": "16.4.2", "react-dom": "16.4.2",
"react-measure": "^2.3.0", "react-measure": "^2.3.0",
"react-scripts-ts": "2.17.0", "react-scripts-ts": "2.17.0",
"react-svg": "^4.1.1", "react-svg": "^4.1.1",
"stable": "^0.1.8", "stable": "^0.1.8",
"blakejs": "^1.1.0" "tslint": "^6.1.0"
}, },
"scripts": { "scripts": {
"precommit": "lint-staged",
"start": "react-scripts-ts start", "start": "react-scripts-ts start",
"build": "react-scripts-ts build", "build": "react-scripts-ts build",
"test": "echo 'FIXME: FE tests need updating'", "test": "echo 'FIXME: FE tests need updating'",
"eject": "react-scripts-ts eject" "eject": "react-scripts-ts eject",
"pretty:check": "prettier --check src/**/*.{ts,tsx}",
"pretty:fix": "prettier --write src"
}, },
"devDependencies": { "devDependencies": {
"@types/jest": "^23.0.2", "@types/jest": "^23.0.2",
@@ -52,6 +58,14 @@
"mock-socket": "^8.0.2", "mock-socket": "^8.0.2",
"react-test-renderer": "^16.5.2", "react-test-renderer": "^16.5.2",
"ts-jest": "^23.10.2", "ts-jest": "^23.10.2",
"tslint-config-prettier": "^1.18.0",
"prettier": "^2.0.2",
"tslint-plugin-prettier": "^2.3.0",
"typescript": "^2.9.2" "typescript": "^2.9.2"
},
"lint-staged": {
"src/**/*.{ts,tsx,json,css}": [
"pretty:fix"
]
} }
} }
+62 -37
View File
@@ -8,19 +8,19 @@ export class AfgHandling {
private updateState: UpdateBound; private updateState: UpdateBound;
private getState: () => Readonly<State>; private getState: () => Readonly<State>;
constructor( constructor(updateState: UpdateBound, getState: () => Readonly<State>) {
updateState: UpdateBound,
getState: () => Readonly<State>,
) {
this.updateState = updateState; this.updateState = updateState;
this.getState = getState; this.getState = getState;
} }
public receivedAuthoritySet( public receivedAuthoritySet(
authoritySetId: Types.AuthoritySetId, authoritySetId: Types.AuthoritySetId,
authorities: Types.Authorities, authorities: Types.Authorities
) { ) {
if (this.getState().authoritySetId != null && authoritySetId !== this.getState().authoritySetId) { if (
this.getState().authoritySetId != null &&
authoritySetId !== this.getState().authoritySetId
) {
// the visualization is restarted when we receive a new auhority set // the visualization is restarted when we receive a new auhority set
this.updateState({ this.updateState({
authoritySetId, authoritySetId,
@@ -43,12 +43,12 @@ export class AfgHandling {
public receivedFinalized( public receivedFinalized(
addr: Types.Address, addr: Types.Address,
finalizedNumber: Types.BlockNumber, finalizedNumber: Types.BlockNumber,
finalizedHash: Types.BlockHash, finalizedHash: Types.BlockHash
) { ) {
const state = this.getState(); const state = this.getState();
if (finalizedNumber < state.best - BLOCKS_LIMIT) { if (finalizedNumber < state.best - BLOCKS_LIMIT) {
return; return;
}; }
const data = { const data = {
Finalized: true, Finalized: true,
@@ -62,15 +62,26 @@ export class AfgHandling {
Prevote: true, Prevote: true,
Precommit: true, Precommit: true,
} as Types.ConsensusDetail; } as Types.ConsensusDetail;
this.initialiseConsensusView(state.consensusInfo, finalizedNumber, addr, addr); this.initialiseConsensusView(
state.consensusInfo,
finalizedNumber,
addr,
addr
);
this.updateConsensusInfo(state.consensusInfo, finalizedNumber, addr, addr, data as Partial<Types.ConsensusDetail>); this.updateConsensusInfo(
state.consensusInfo,
finalizedNumber,
addr,
addr,
data as Partial<Types.ConsensusDetail>
);
// Finalizing a block implicitly includes finalizing all // Finalizing a block implicitly includes finalizing all
// preceding blocks. This function marks the preceding // preceding blocks. This function marks the preceding
// blocks as implicitly finalized on and stores a pointer // blocks as implicitly finalized on and stores a pointer
// to the block which contains the explicit finalization. // to the block which contains the explicit finalization.
const op = (i: Types.BlockNumber, index: number) : boolean => { const op = (i: Types.BlockNumber, index: number): boolean => {
const consensusDetail = state.consensusInfo[index][1][addr][addr]; const consensusDetail = state.consensusInfo[index][1][addr][addr];
if (consensusDetail.Finalized || consensusDetail.ImplicitFinalized) { if (consensusDetail.Finalized || consensusDetail.ImplicitFinalized) {
return false; return false;
@@ -96,7 +107,7 @@ export class AfgHandling {
this.backfill(state.consensusInfo, finalizedNumber, op, addr, addr); this.backfill(state.consensusInfo, finalizedNumber, op, addr, addr);
this.pruneBlocks(state.consensusInfo); this.pruneBlocks(state.consensusInfo);
this.updateState({consensusInfo: state.consensusInfo}); this.updateState({ consensusInfo: state.consensusInfo });
} }
public receivedPre( public receivedPre(
@@ -104,37 +115,48 @@ export class AfgHandling {
height: Types.BlockNumber, height: Types.BlockNumber,
hash: Types.BlockHash, hash: Types.BlockHash,
voter: Types.Address, voter: Types.Address,
what: string, what: string
) { ) {
const state = this.getState(); const state = this.getState();
if (height < state.best - BLOCKS_LIMIT) { if (height < state.best - BLOCKS_LIMIT) {
return; return;
}; }
const data = what === "prevote" ? { Prevote: true } : { Precommit: true }; const data = what === 'prevote' ? { Prevote: true } : { Precommit: true };
this.initialiseConsensusView(state.consensusInfo, height, addr, voter); this.initialiseConsensusView(state.consensusInfo, height, addr, voter);
this.updateConsensusInfo(state.consensusInfo, height, addr, voter, data as Partial<Types.ConsensusDetail>); this.updateConsensusInfo(
state.consensusInfo,
height,
addr,
voter,
data as Partial<Types.ConsensusDetail>
);
// A Prevote or Precommit on a block implicitly includes // A Prevote or Precommit on a block implicitly includes
// a vote on all preceding blocks. This function marks // a vote on all preceding blocks. This function marks
// the preceding blocks as implicitly voted on and stores // the preceding blocks as implicitly voted on and stores
// a pointer to the block which contains the explicit vote. // a pointer to the block which contains the explicit vote.
const op = (i: Types.BlockNumber, index: number) : boolean => { const op = (i: Types.BlockNumber, index: number): boolean => {
const consensusDetail = state.consensusInfo[index][1][addr][voter]; const consensusDetail = state.consensusInfo[index][1][addr][voter];
if (what === "prevote" && (consensusDetail.Prevote || consensusDetail.ImplicitPrevote)) { if (
what === 'prevote' &&
(consensusDetail.Prevote || consensusDetail.ImplicitPrevote)
) {
return false; return false;
} }
if (what === "precommit" && (consensusDetail.Precommit || consensusDetail.ImplicitPrecommit) if (
what === 'precommit' &&
// because of extrapolation a prevote needs to be set as well. (consensusDetail.Precommit || consensusDetail.ImplicitPrecommit) &&
// if it is not we continue backfilling (and set it during that process). // because of extrapolation a prevote needs to be set as well.
&& (consensusDetail.Prevote || consensusDetail.ImplicitPrevote)) { // if it is not we continue backfilling (and set it during that process).
(consensusDetail.Prevote || consensusDetail.ImplicitPrevote)
) {
return false; return false;
} }
if (what === "prevote") { if (what === 'prevote') {
consensusInfo[index][1][addr][voter].ImplicitPrevote = true; consensusInfo[index][1][addr][voter].ImplicitPrevote = true;
} else if (what === "precommit") { } else if (what === 'precommit') {
consensusInfo[index][1][addr][voter].ImplicitPrecommit = true; consensusInfo[index][1][addr][voter].ImplicitPrecommit = true;
// Extrapolate. Precommit implies Prevote. // Extrapolate. Precommit implies Prevote.
@@ -147,7 +169,7 @@ export class AfgHandling {
this.backfill(consensusInfo, height, op, addr, voter); this.backfill(consensusInfo, height, op, addr, voter);
this.pruneBlocks(consensusInfo); this.pruneBlocks(consensusInfo);
this.updateState({consensusInfo}); this.updateState({ consensusInfo });
} }
// Initializes the `ConsensusView` with empty objects. // Initializes the `ConsensusView` with empty objects.
@@ -155,10 +177,9 @@ export class AfgHandling {
consensusInfo: Types.ConsensusInfo, consensusInfo: Types.ConsensusInfo,
height: Types.BlockNumber, height: Types.BlockNumber,
own: Types.Address, own: Types.Address,
other: Types.Address, other: Types.Address
) { ) {
const found = const found = consensusInfo.find(([blockNumber]) => blockNumber === height);
consensusInfo.find(([blockNumber,]) => blockNumber === height);
let consensusView; let consensusView;
if (found) { if (found) {
@@ -170,7 +191,9 @@ export class AfgHandling {
this.initialiseConsensusViewByRef(consensusView, own, other); this.initialiseConsensusViewByRef(consensusView, own, other);
const item: Types.ConsensusItem = [height, consensusView]; const item: Types.ConsensusItem = [height, consensusView];
const insertPos = consensusInfo.findIndex(([elHeight, elView]) => elHeight < height); const insertPos = consensusInfo.findIndex(
([elHeight, elView]) => elHeight < height
);
if (insertPos >= 0) { if (insertPos >= 0) {
consensusInfo.splice(insertPos, 0, item); consensusInfo.splice(insertPos, 0, item);
} else { } else {
@@ -183,7 +206,7 @@ export class AfgHandling {
private initialiseConsensusViewByRef( private initialiseConsensusViewByRef(
consensusView: Types.ConsensusView, consensusView: Types.ConsensusView,
own: Types.Address, own: Types.Address,
other: Types.Address, other: Types.Address
) { ) {
if (!consensusView[own]) { if (!consensusView[own]) {
consensusView[own] = {} as Types.ConsensusState; consensusView[own] = {} as Types.ConsensusState;
@@ -204,7 +227,7 @@ export class AfgHandling {
start: Types.BlockNumber, start: Types.BlockNumber,
f: (i: Types.BlockNumber, index: number) => boolean, f: (i: Types.BlockNumber, index: number) => boolean,
own: Types.Address, own: Types.Address,
other: Types.Address, other: Types.Address
) { ) {
// if this is the first block then we don't fill latter blocks // if this is the first block then we don't fill latter blocks
// if there is only one block, then it also doesn't make // if there is only one block, then it also doesn't make
@@ -241,8 +264,9 @@ export class AfgHandling {
const startBlockNumber = start as Types.BlockNumber; const startBlockNumber = start as Types.BlockNumber;
this.initialiseConsensusView(consensusInfo, startBlockNumber, own, other); this.initialiseConsensusView(consensusInfo, startBlockNumber, own, other);
const index = const index = consensusInfo.findIndex(
consensusInfo.findIndex(([blockNumber,]) => blockNumber === start); ([blockNumber]) => blockNumber === start
);
const cont = f(start, index); const cont = f(start, index);
if (!cont) { if (!cont) {
break; break;
@@ -261,10 +285,11 @@ export class AfgHandling {
height: Types.BlockNumber, height: Types.BlockNumber,
addr: Types.Address, addr: Types.Address,
voter: Types.Address, voter: Types.Address,
data: Partial<Types.ConsensusDetail>, data: Partial<Types.ConsensusDetail>
) { ) {
const found = const found = consensusInfo.findIndex(
consensusInfo.findIndex(([blockNumber,]) => blockNumber === height); ([blockNumber]) => blockNumber === height
);
if (found < 0) { if (found < 0) {
return; return;
} }
+45 -28
View File
@@ -52,8 +52,8 @@ export default class App extends React.Component<{}, State> {
const selectedColumns = this.selectedColumns(settings); const selectedColumns = this.selectedColumns(settings);
this.sortBy.set(null); this.sortBy.set(null);
this.setState({ settings, selectedColumns, sortBy: null }) this.setState({ settings, selectedColumns, sortBy: null });
}, }
); );
this.pins = new PersistentSet<Types.NodeName>('pinned_names', (pins) => { this.pins = new PersistentSet<Types.NodeName>('pinned_names', (pins) => {
@@ -105,7 +105,7 @@ export default class App extends React.Component<{}, State> {
return this.state; return this.state;
}); });
setInterval(() => this.chainsCache = [], 10000); // Wipe sorted chains cache every 10 seconds setInterval(() => (this.chainsCache = []), 10000); // Wipe sorted chains cache every 10 seconds
} }
public render() { public render() {
@@ -123,15 +123,30 @@ export default class App extends React.Component<{}, State> {
); );
} }
const overlay = tab === 'all-chains' const overlay =
? <AllChains chains={chains} subscribed={subscribed} connection={this.connection} /> tab === 'all-chains' ? (
: null; <AllChains
chains={chains}
subscribed={subscribed}
connection={this.connection}
/>
) : null;
return ( return (
<div className="App"> <div className="App">
<OfflineIndicator status={status} /> <OfflineIndicator status={status} />
<Chains chains={chains} subscribed={subscribed} connection={this.connection} /> <Chains
<Chain appState={this.state} connection={this.connection} settings={this.settings} pins={this.pins} sortBy={this.sortBy} /> chains={chains}
subscribed={subscribed}
connection={this.connection}
/>
<Chain
appState={this.state}
connection={this.connection}
settings={this.settings}
pins={this.pins}
sortBy={this.sortBy}
/>
{overlay} {overlay}
</div> </div>
); );
@@ -148,7 +163,8 @@ export default class App extends React.Component<{}, State> {
} }
private onKeyPress = (event: KeyboardEvent) => { private onKeyPress = (event: KeyboardEvent) => {
if (event.keyCode !== 9) { // TAB KEY if (event.keyCode !== 9) {
// TAB KEY
return; return;
} }
@@ -170,41 +186,42 @@ export default class App extends React.Component<{}, State> {
this.connection.then((connection) => { this.connection.then((connection) => {
connection.subscribe(chains[index]); connection.subscribe(chains[index]);
}) });
} };
private onHashChange = (event: Event) => { private onHashChange = (event: Event) => {
const { tab = '' } = getHashData(); const { tab = '' } = getHashData();
this.setState({ tab }); this.setState({ tab });
} };
private chains(): ChainData[] { private chains(): ChainData[] {
if (this.chainsCache.length === this.state.chains.size) { if (this.chainsCache.length === this.state.chains.size) {
return this.chainsCache; return this.chainsCache;
} }
this.chainsCache = stable this.chainsCache = stable.inplace(
.inplace( Array.from(this.state.chains.values()),
Array.from(this.state.chains.values()), (a, b) => {
(a, b) => { if (a.label === PINNED_CHAIN) {
if (a.label === PINNED_CHAIN) { return -1;
return -1;
}
if (b.label === PINNED_CHAIN) {
return 1;
}
return b.nodeCount - a.nodeCount;
} }
);
if (b.label === PINNED_CHAIN) {
return 1;
}
return b.nodeCount - a.nodeCount;
}
);
return this.chainsCache; return this.chainsCache;
} }
private selectedColumns(settings: State.Settings): Column[] { private selectedColumns(settings: State.Settings): Column[] {
return Row.columns.filter(({ setting }) => setting == null || settings[setting]); return Row.columns.filter(
({ setting }) => setting == null || settings[setting]
);
} }
private getComparator(sortBy: Maybe<number>): Compare<Node> { private getComparator(sortBy: Maybe<number>): Compare<Node> {
@@ -228,7 +245,7 @@ export default class App extends React.Component<{}, State> {
} else { } else {
return Node.compare(a, b); return Node.compare(a, b);
} }
} };
} }
} }
+86 -34
View File
@@ -1,5 +1,19 @@
import { VERSION, timestamp, FeedMessage, Types, Maybe, sleep } from '@dotstats/common'; import {
import { State, Update, Node, UpdateBound, ChainData, PINNED_CHAIN } from './state'; VERSION,
timestamp,
FeedMessage,
Types,
Maybe,
sleep,
} from '@dotstats/common';
import {
State,
Update,
Node,
UpdateBound,
ChainData,
PINNED_CHAIN,
} from './state';
import { PersistentSet } from './persist'; import { PersistentSet } from './persist';
import { getHashData, setHashData } from './utils'; import { getHashData, setHashData } from './utils';
import { AfgHandling } from './AfgHandling'; import { AfgHandling } from './AfgHandling';
@@ -12,15 +26,19 @@ const TIMEOUT_BASE = (1000 * 5) as Types.Milliseconds; // 5 seconds
const TIMEOUT_MAX = (1000 * 60 * 5) as Types.Milliseconds; // 5 minutes const TIMEOUT_MAX = (1000 * 60 * 5) as Types.Milliseconds; // 5 minutes
export class Connection { export class Connection {
public static async create(pins: PersistentSet<Types.NodeName>, update: Update): Promise<Connection> { public static async create(
pins: PersistentSet<Types.NodeName>,
update: Update
): Promise<Connection> {
return new Connection(await Connection.socket(), update, pins); return new Connection(await Connection.socket(), update, pins);
} }
private static readonly utf8decoder = new TextDecoder('utf-8'); private static readonly utf8decoder = new TextDecoder('utf-8');
private static readonly address = window.location.protocol === 'https:' private static readonly address =
? `wss://${window.location.hostname}/feed/` window.location.protocol === 'https:'
: `ws://127.0.0.1:8000/feed`; ? `wss://${window.location.hostname}/feed/`
: `ws://127.0.0.1:8000/feed`;
// private static readonly address = 'wss://telemetry.polkadot.io/feed/'; // private static readonly address = 'wss://telemetry.polkadot.io/feed/';
@@ -58,7 +76,7 @@ export class Connection {
const socket = new WebSocket(Connection.address); const socket = new WebSocket(Connection.address);
socket.binaryType = "arraybuffer"; socket.binaryType = 'arraybuffer';
socket.addEventListener('open', onSuccess); socket.addEventListener('open', onSuccess);
socket.addEventListener('error', onFailure); socket.addEventListener('error', onFailure);
socket.addEventListener('close', onFailure); socket.addEventListener('close', onFailure);
@@ -75,7 +93,11 @@ export class Connection {
private readonly update: Update; private readonly update: Update;
private readonly pins: PersistentSet<Types.NodeName>; private readonly pins: PersistentSet<Types.NodeName>;
constructor(socket: WebSocket, update: Update, pins: PersistentSet<Types.NodeName>) { constructor(
socket: WebSocket,
update: Update,
pins: PersistentSet<Types.NodeName>
) {
this.socket = socket; this.socket = socket;
this.update = update; this.update = update;
this.pins = pins; this.pins = pins;
@@ -97,7 +119,7 @@ export class Connection {
public subscribeConsensus(chain: Types.ChainLabel) { public subscribeConsensus(chain: Types.ChainLabel) {
if (this.state.authorities.length <= VIS_AUTHORITIES_LIMIT) { if (this.state.authorities.length <= VIS_AUTHORITIES_LIMIT) {
setHashData({chain}); setHashData({ chain });
this.resubscribeSendFinality = true; this.resubscribeSendFinality = true;
this.socket.send(`send-finality:${chain}`); this.socket.send(`send-finality:${chain}`);
} }
@@ -121,14 +143,17 @@ export class Connection {
const { nodes, chains, sortBy, selectedColumns } = this.state; const { nodes, chains, sortBy, selectedColumns } = this.state;
const ref = nodes.ref(); const ref = nodes.ref();
const updateState: UpdateBound = (state) => { this.state = this.update(state); }; const updateState: UpdateBound = (state) => {
this.state = this.update(state);
};
const getState = () => this.state; const getState = () => this.state;
const afg = new AfgHandling(updateState, getState); const afg = new AfgHandling(updateState, getState);
let sortByColumn: Maybe<Column> = null; let sortByColumn: Maybe<Column> = null;
if (sortBy != null) { if (sortBy != null) {
sortByColumn = sortBy < 0 ? selectedColumns[~sortBy] : selectedColumns[sortBy]; sortByColumn =
sortBy < 0 ? selectedColumns[~sortBy] : selectedColumns[sortBy];
} }
for (const message of messages) { for (const message of messages) {
@@ -166,9 +191,28 @@ export class Connection {
} }
case Actions.AddedNode: { case Actions.AddedNode: {
const [id, nodeDetails, nodeStats, nodeIO, nodeHardware, blockDetails, location, connectedAt] = message.payload; const [
id,
nodeDetails,
nodeStats,
nodeIO,
nodeHardware,
blockDetails,
location,
connectedAt,
] = message.payload;
const pinned = this.pins.has(nodeDetails[0]); const pinned = this.pins.has(nodeDetails[0]);
const node = new Node(pinned, id, nodeDetails, nodeStats, nodeIO, nodeHardware, blockDetails, location, connectedAt); const node = new Node(
pinned,
id,
nodeDetails,
nodeStats,
nodeIO,
nodeHardware,
blockDetails,
location,
connectedAt
);
nodes.add(node); nodes.add(node);
@@ -194,7 +238,11 @@ export class Connection {
case Actions.LocatedNode: { case Actions.LocatedNode: {
const [id, lat, lon, city] = message.payload; const [id, lat, lon, city] = message.payload;
nodes.mutAndMaybeSort(id, (node) => node.updateLocation([lat, lon, city]), sortByColumn === Column.LOCATION); nodes.mutAndMaybeSort(
id,
(node) => node.updateLocation([lat, lon, city]),
sortByColumn === Column.LOCATION
);
break; break;
} }
@@ -213,7 +261,8 @@ export class Connection {
nodes.mutAndMaybeSort( nodes.mutAndMaybeSort(
id, id,
(node) => node.updateFinalized(height, hash), (node) => node.updateFinalized(height, hash),
sortByColumn === Column.FINALIZED || sortByColumn === Column.FINALIZED_HASH, sortByColumn === Column.FINALIZED ||
sortByColumn === Column.FINALIZED_HASH
); );
break; break;
@@ -225,7 +274,7 @@ export class Connection {
nodes.mutAndMaybeSort( nodes.mutAndMaybeSort(
id, id,
(node) => node.updateStats(nodeStats), (node) => node.updateStats(nodeStats),
sortByColumn === Column.PEERS || sortByColumn === Column.TXS, sortByColumn === Column.PEERS || sortByColumn === Column.TXS
); );
break; break;
@@ -237,10 +286,10 @@ export class Connection {
nodes.mutAndMaybeSort( nodes.mutAndMaybeSort(
id, id,
(node) => node.updateHardware(nodeHardware), (node) => node.updateHardware(nodeHardware),
sortByColumn === Column.CPU sortByColumn === Column.CPU ||
|| sortByColumn === Column.MEM sortByColumn === Column.MEM ||
|| sortByColumn === Column.UPLOAD sortByColumn === Column.UPLOAD ||
|| sortByColumn === Column.DOWNLOAD, sortByColumn === Column.DOWNLOAD
); );
break; break;
@@ -252,10 +301,10 @@ export class Connection {
nodes.mutAndMaybeSort( nodes.mutAndMaybeSort(
id, id,
(node) => node.updateIO(nodeIO), (node) => node.updateIO(nodeIO),
sortByColumn === Column.STATE_CACHE sortByColumn === Column.STATE_CACHE ||
|| sortByColumn === Column.DB_CACHE sortByColumn === Column.DB_CACHE ||
|| sortByColumn === Column.DISK_READ sortByColumn === Column.DISK_READ ||
|| sortByColumn === Column.DISK_WRITE, sortByColumn === Column.DISK_WRITE
); );
break; break;
@@ -263,7 +312,7 @@ export class Connection {
case Actions.TimeSync: { case Actions.TimeSync: {
this.state = this.update({ this.state = this.update({
timeDiff: (timestamp() - message.payload) as Types.Milliseconds timeDiff: (timestamp() - message.payload) as Types.Milliseconds,
}); });
break; break;
@@ -331,7 +380,7 @@ export class Connection {
case Actions.AfgReceivedPrevote: { case Actions.AfgReceivedPrevote: {
const [nodeAddress, blockNumber, blockHash, voter] = message.payload; const [nodeAddress, blockNumber, blockHash, voter] = message.payload;
const no = parseInt(String(blockNumber), 10) as Types.BlockNumber; const no = parseInt(String(blockNumber), 10) as Types.BlockNumber;
afg.receivedPre(nodeAddress, no, blockHash, voter, "prevote"); afg.receivedPre(nodeAddress, no, blockHash, voter, 'prevote');
break; break;
} }
@@ -339,7 +388,7 @@ export class Connection {
case Actions.AfgReceivedPrecommit: { case Actions.AfgReceivedPrecommit: {
const [nodeAddress, blockNumber, blockHash, voter] = message.payload; const [nodeAddress, blockNumber, blockHash, voter] = message.payload;
const no = parseInt(String(blockNumber), 10) as Types.BlockNumber; const no = parseInt(String(blockNumber), 10) as Types.BlockNumber;
afg.receivedPre(nodeAddress, no, blockHash, voter, "precommit"); afg.receivedPre(nodeAddress, no, blockHash, voter, 'precommit');
break; break;
} }
@@ -362,7 +411,7 @@ export class Connection {
} }
this.autoSubscribe(); this.autoSubscribe();
} };
private bindSocket() { private bindSocket() {
this.ping(); this.ping();
@@ -398,7 +447,7 @@ export class Connection {
this.socket.send(`ping:${this.pingId}`); this.socket.send(`ping:${this.pingId}`);
this.pingTimeout = setTimeout(this.ping, 30000); this.pingTimeout = setTimeout(this.ping, 30000);
} };
private pong(id: number) { private pong(id: number) {
if (!this.pingSent) { if (!this.pingSent) {
@@ -430,12 +479,15 @@ export class Connection {
} }
private handleFeedData = (event: MessageEvent) => { private handleFeedData = (event: MessageEvent) => {
const data = typeof event.data === 'string' const data =
? event.data as any as FeedMessage.Data typeof event.data === 'string'
: Connection.utf8decoder.decode(event.data) as any as FeedMessage.Data; ? ((event.data as any) as FeedMessage.Data)
: ((Connection.utf8decoder.decode(
event.data
) as any) as FeedMessage.Data);
this.handleMessages(FeedMessage.deserialize(data)); this.handleMessages(FeedMessage.deserialize(data));
} };
private autoSubscribe() { private autoSubscribe() {
const { subscribed, chains } = this.state; const { subscribed, chains } = this.state;
@@ -480,5 +532,5 @@ export class Connection {
this.socket.close(); this.socket.close();
this.socket = await Connection.socket(); this.socket = await Connection.socket();
this.bindSocket(); this.bindSocket();
} };
} }
+13 -11
View File
@@ -4,12 +4,12 @@ import { timestamp, Types } from '@dotstats/common';
export namespace Ago { export namespace Ago {
export interface Props { export interface Props {
when: Types.Timestamp, when: Types.Timestamp;
justTime?: boolean, justTime?: boolean;
} }
export interface State { export interface State {
now: Types.Timestamp, now: Types.Timestamp;
} }
} }
@@ -29,7 +29,7 @@ tick();
export namespace Ago { export namespace Ago {
export interface State { export interface State {
now: Types.Timestamp now: Types.Timestamp;
} }
} }
@@ -42,16 +42,16 @@ export class Ago extends React.Component<Ago.Props, Ago.State> {
super(props); super(props);
this.state = { this.state = {
now: (timestamp() - Ago.timeDiff) as Types.Timestamp now: (timestamp() - Ago.timeDiff) as Types.Timestamp,
}; };
} }
public componentWillMount() { public componentWillMount() {
tickers.set(this, (now) => { tickers.set(this, (now) => {
this.setState({ this.setState({
now: (now - Ago.timeDiff) as Types.Timestamp now: (now - Ago.timeDiff) as Types.Timestamp,
}); });
}) });
} }
public componentWillUnmount() { public componentWillUnmount() {
@@ -72,17 +72,19 @@ export class Ago extends React.Component<Ago.Props, Ago.State> {
} else if (ago < 60) { } else if (ago < 60) {
agoStr = `${ago | 0}s`; agoStr = `${ago | 0}s`;
} else if (ago < 3600) { } else if (ago < 3600) {
agoStr = `${ ago / 60 | 0}m`; agoStr = `${(ago / 60) | 0}m`;
} else if (ago < 3600 * 24) { } else if (ago < 3600 * 24) {
agoStr = `${ ago / 3600 | 0}h`; agoStr = `${(ago / 3600) | 0}h`;
} else { } else {
agoStr = `${ ago / (3600 * 24) | 0}d`; agoStr = `${(ago / (3600 * 24)) | 0}d`;
} }
if (this.props.justTime !== true) { if (this.props.justTime !== true) {
agoStr += ' ago'; agoStr += ' ago';
} }
return <span title={new Date(this.props.when).toUTCString()}>{agoStr}</span> return (
<span title={new Date(this.props.when).toUTCString()}>{agoStr}</span>
);
} }
} }
@@ -8,7 +8,7 @@
width: 25vw; width: 25vw;
min-width: 300px; min-width: 300px;
background: #fff; background: #fff;
box-shadow: 0 2px 20px rgba(0,0,0,0.35); box-shadow: 0 2px 20px rgba(0, 0, 0, 0.35);
overflow-y: scroll; overflow-y: scroll;
overflow-x: hide; overflow-x: hide;
} }
@@ -17,7 +17,7 @@
position: fixed; position: fixed;
display: block; display: block;
z-index: 19; z-index: 19;
background: rgba(0,0,0,0.35); background: rgba(0, 0, 0, 0.35);
left: 0; left: 0;
right: 0; right: 0;
top: 0; top: 0;
@@ -26,10 +26,10 @@
.AllChains-chain { .AllChains-chain {
padding: 0 12px; padding: 0 12px;
background: #B5AEAE; background: #b5aeae;
color: #444; color: #444;
display: block; display: block;
border-bottom: 1px solid rgba(255,255,255,0.5); border-bottom: 1px solid rgba(255, 255, 255, 0.5);
height: 40px; height: 40px;
line-height: 40px; line-height: 40px;
cursor: pointer; cursor: pointer;
@@ -42,10 +42,10 @@
display: inline-block; display: inline-block;
padding: 0 0.5em 0.1em; padding: 0 0.5em 0.1em;
border-radius: 1em; border-radius: 1em;
background: #8C8787; background: #8c8787;
color: #fff; color: #fff;
font-weight: normal; font-weight: normal;
text-shadow: rgba(0,0,0,0.5) 0 1px 0; text-shadow: rgba(0, 0, 0, 0.5) 0 1px 0;
font-size: 0.9em; font-size: 0.9em;
line-height: 1.4em; line-height: 1.4em;
margin: 0 -0.3em 0 0.3em; margin: 0 -0.3em 0 0.3em;
@@ -57,5 +57,5 @@
} }
.AllChains-chain-selected .AllChains-node-count { .AllChains-chain-selected .AllChains-node-count {
background: #E6007A; background: #e6007a;
} }
+17 -9
View File
@@ -7,9 +7,9 @@ import './AllChains.css';
export namespace AllChains { export namespace AllChains {
export interface Props { export interface Props {
chains: ChainData[], chains: ChainData[];
subscribed: Maybe<Types.ChainLabel>, subscribed: Maybe<Types.ChainLabel>;
connection: Promise<Connection> connection: Promise<Connection>;
} }
} }
@@ -31,15 +31,23 @@ export class AllChains extends React.Component<AllChains.Props, {}> {
private renderChain(chain: ChainData): React.ReactNode { private renderChain(chain: ChainData): React.ReactNode {
const { label, nodeCount } = chain; const { label, nodeCount } = chain;
const className = label === this.props.subscribed const className =
? 'AllChains-chain AllChains-chain-selected' label === this.props.subscribed
: 'AllChains-chain'; ? 'AllChains-chain AllChains-chain-selected'
: 'AllChains-chain';
return ( return (
<a key={label} className={className} onClick={this.subscribe.bind(this, label)}> <a
{label} <span className="AllChains-node-count" title="Node Count">{nodeCount}</span> key={label}
className={className}
onClick={this.subscribe.bind(this, label)}
>
{label}{' '}
<span className="AllChains-node-count" title="Node Count">
{nodeCount}
</span>
</a> </a>
) );
} }
private async subscribe(chain: Types.ChainLabel) { private async subscribe(chain: Types.ChainLabel) {
@@ -28,7 +28,7 @@
width: 100%; width: 100%;
min-width: 1350px; min-width: 1350px;
min-height: 100%; min-height: 100%;
background: #2C2B2B; background: #2c2b2b;
color: #fff; color: #fff;
box-shadow: rgba(0,0,0,0.5) 0 3px 30px; box-shadow: rgba(0, 0, 0, 0.5) 0 3px 30px;
} }
@@ -65,21 +65,57 @@ export class Chain extends React.Component<Chain.Props, Chain.State> {
return ( return (
<div className="Chain"> <div className="Chain">
<div className="Chain-header"> <div className="Chain-header">
<Tile icon={blockIcon} title="Best Block">#{formatNumber(best)}</Tile> <Tile icon={blockIcon} title="Best Block">
<Tile icon={finalizedIcon} title="Finalized Block">#{formatNumber(finalized)}</Tile> #{formatNumber(best)}
<Tile icon={blockTimeIcon} title="Average Time">{ blockAverage == null ? '-' : secondsWithPrecision(blockAverage / 1000) }</Tile> </Tile>
<Tile icon={lastTimeIcon} title="Last Block"><Ago when={blockTimestamp} /></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"> <div className="Chain-tabs">
<Tab icon={listIcon} label="List" display="list" tab="" current={currentTab} setDisplay={this.setDisplay} /> <Tab
<Tab icon={worldIcon} label="Map" display="map" tab="map" current={currentTab} setDisplay={this.setDisplay} /> icon={listIcon}
<Tab icon={consensusIcon} label="Consensus" display="consensus" tab="consensus" current={currentTab} setDisplay={this.setDisplay} /> label="List"
<Tab icon={settingsIcon} label="Settings" display="settings" tab="settings" current={currentTab} setDisplay={this.setDisplay} /> 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>
</div> </div>
<div className="Chain-content-container"> <div className="Chain-content-container">
<div className="Chain-content"> <div className="Chain-content">{this.renderContent()}</div>
{this.renderContent()}
</div>
</div> </div>
</div> </div>
); );
@@ -98,10 +134,10 @@ export class Chain extends React.Component<Chain.Props, Chain.State> {
return <Consensus appState={appState} connection={connection} />; return <Consensus appState={appState} connection={connection} />;
} }
return ( return display === 'list' ? (
display === 'list' <List appState={appState} pins={pins} sortBy={sortBy} />
? <List appState={appState} pins={pins} sortBy={sortBy} /> ) : (
: <Map appState={appState} /> <Map appState={appState} />
); );
} }
@@ -1,4 +1,3 @@
.Chain-Tab { .Chain-Tab {
display: inline-block; display: inline-block;
} }
@@ -18,7 +17,8 @@
background: #ccc; background: #ccc;
} }
.Chain-Tab-on .Icon, .Chain-Tab-on:hover .Icon { .Chain-Tab-on .Icon,
background: #E6007A; .Chain-Tab-on:hover .Icon {
background: #e6007a;
color: #fff; color: #fff;
} }
@@ -33,5 +33,5 @@ export class Tab extends React.Component<Tab.Props, {}> {
const { tab, display, setDisplay } = this.props; const { tab, display, setDisplay } = this.props;
setHashData({ tab }); setHashData({ tab });
setDisplay(display); setDisplay(display);
} };
} }
+9 -8
View File
@@ -1,5 +1,5 @@
.Chains { .Chains {
background: #B5AEAE; background: #b5aeae;
color: #000; color: #000;
padding: 0 76px 0 16px; padding: 0 76px 0 16px;
height: 40px; height: 40px;
@@ -9,10 +9,10 @@
.Chains-chain { .Chains-chain {
padding: 0 12px; padding: 0 12px;
background: #B5AEAE; background: #b5aeae;
color: #444; color: #444;
display: inline-block; display: inline-block;
border-right: 1px solid rgba(255,255,255,0.5); border-right: 1px solid rgba(255, 255, 255, 0.5);
height: 40px; height: 40px;
line-height: 40px; line-height: 40px;
cursor: pointer; cursor: pointer;
@@ -22,7 +22,7 @@
} }
.Chains-chain:first-child { .Chains-chain:first-child {
border-left: 1px solid rgba(255,255,255,0.5); border-left: 1px solid rgba(255, 255, 255, 0.5);
} }
.Chains-all-chains { .Chains-all-chains {
@@ -43,7 +43,8 @@
top: 6px; top: 6px;
} }
.Chains-all-chains .Icon, .Chains-fork-me .Icon { .Chains-all-chains .Icon,
.Chains-fork-me .Icon {
font-size: 28px; font-size: 28px;
margin: 0; margin: 0;
height: 28px; height: 28px;
@@ -55,10 +56,10 @@
display: inline-block; display: inline-block;
padding: 0 0.5em 0.1em; padding: 0 0.5em 0.1em;
border-radius: 1em; border-radius: 1em;
background: #8C8787; background: #8c8787;
color: #fff; color: #fff;
font-weight: normal; font-weight: normal;
text-shadow: rgba(0,0,0,0.5) 0 1px 0; text-shadow: rgba(0, 0, 0, 0.5) 0 1px 0;
font-size: 0.9em; font-size: 0.9em;
line-height: 1.4em; line-height: 1.4em;
margin: 0 -0.3em 0 0.3em; margin: 0 -0.3em 0 0.3em;
@@ -70,5 +71,5 @@
} }
.Chains-chain-selected .Chains-node-count { .Chains-chain-selected .Chains-node-count {
background: #E6007A; background: #e6007a;
} }
+25 -11
View File
@@ -10,15 +10,17 @@ import './Chains.css';
export namespace Chains { export namespace Chains {
export interface Props { export interface Props {
chains: ChainData[], chains: ChainData[];
subscribed: Maybe<Types.ChainLabel>, subscribed: Maybe<Types.ChainLabel>;
connection: Promise<Connection> connection: Promise<Connection>;
} }
} }
export class Chains extends React.Component<Chains.Props, {}> { export class Chains extends React.Component<Chains.Props, {}> {
public render() { public render() {
const allChainsHref = this.props.subscribed ? `#all-chains/${this.props.subscribed}` : `#all-chains`; const allChainsHref = this.props.subscribed
? `#all-chains/${this.props.subscribed}`
: `#all-chains`;
const { chains } = this.props; const { chains } = this.props;
return ( return (
@@ -27,7 +29,11 @@ export class Chains extends React.Component<Chains.Props, {}> {
<a className="Chains-all-chains" href={allChainsHref}> <a className="Chains-all-chains" href={allChainsHref}>
<Icon src={listIcon} alt="All Chains" /> <Icon src={listIcon} alt="All Chains" />
</a> </a>
<a className="Chains-fork-me" href="https://github.com/paritytech/substrate-telemetry" target="_blank"> <a
className="Chains-fork-me"
href="https://github.com/paritytech/substrate-telemetry"
target="_blank"
>
<Icon src={githubIcon} alt="Fork Me!" /> <Icon src={githubIcon} alt="Fork Me!" />
</a> </a>
</div> </div>
@@ -37,15 +43,23 @@ export class Chains extends React.Component<Chains.Props, {}> {
private renderChain(chain: ChainData): React.ReactNode { private renderChain(chain: ChainData): React.ReactNode {
const { label, nodeCount } = chain; const { label, nodeCount } = chain;
const className = label === this.props.subscribed const className =
? 'Chains-chain Chains-chain-selected' label === this.props.subscribed
: 'Chains-chain'; ? 'Chains-chain Chains-chain-selected'
: 'Chains-chain';
return ( return (
<a key={label} className={className} onClick={this.subscribe.bind(this, label)}> <a
{label} <span className="Chains-node-count" title="Node Count">{nodeCount}</span> key={label}
className={className}
onClick={this.subscribe.bind(this, label)}
>
{label}{' '}
<span className="Chains-node-count" title="Node Count">
{nodeCount}
</span>
</a> </a>
) );
} }
private async subscribe(chain: Types.ChainLabel) { private async subscribe(chain: Types.ChainLabel) {
@@ -1,5 +1,5 @@
.Consensus .ConsensusList { .Consensus .ConsensusList {
opacity: 0.0; /* the box should only show up once flexing has been applied */ opacity: 0; /* the box should only show up once flexing has been applied */
} }
.Consensus .ConsensusList table { .Consensus .ConsensusList table {
@@ -10,11 +10,11 @@
display: flex; display: flex;
align-items: stretch; align-items: stretch;
flex-direction: row; flex-direction: row;
opacity: 1.0; opacity: 1;
} }
.Consensus .flexContainerLargeRow .firstInRow { .Consensus .flexContainerLargeRow .firstInRow {
width: 100% width: 100%;
} }
.Consensus .flexContainerLargeRow .firstInRow .emptylegend, .Consensus .flexContainerLargeRow .firstInRow .emptylegend,
@@ -29,7 +29,7 @@
align-items: stretch; align-items: stretch;
flex-direction: row; flex-direction: row;
flex-wrap: wrap; flex-wrap: wrap;
opacity: 1.0; opacity: 1;
} }
.Consensus .flexContainerSmallRow div { .Consensus .flexContainerSmallRow div {
@@ -1,7 +1,7 @@
import * as React from 'react'; import * as React from 'react';
import { Types, Maybe } from '@dotstats/common'; import { Types, Maybe } from '@dotstats/common';
import { Connection } from '../../Connection'; import { Connection } from '../../Connection';
import Measure, {BoundingRect, ContentRect} from 'react-measure'; import Measure, { BoundingRect, ContentRect } from 'react-measure';
import { ConsensusBlock } from './'; import { ConsensusBlock } from './';
import { State as AppState } from '../../state'; import { State as AppState } from '../../state';
@@ -21,16 +21,16 @@ export namespace Consensus {
export interface State { export interface State {
dimensions: BoundingRect; dimensions: BoundingRect;
largeBlockWithLegend: BoundingRect, largeBlockWithLegend: BoundingRect;
largeBlock: BoundingRect, largeBlock: BoundingRect;
countBlocksInLargeRow: number, countBlocksInLargeRow: number;
largeRowsAddFlexClass: boolean, largeRowsAddFlexClass: boolean;
smallBlock: BoundingRect, smallBlock: BoundingRect;
smallBlocksRows: number, smallBlocksRows: number;
countBlocksInSmallRow: number, countBlocksInSmallRow: number;
smallRowsAddFlexClass: boolean, smallRowsAddFlexClass: boolean;
lastConsensusInfo: string, lastConsensusInfo: string;
} }
} }
@@ -48,48 +48,59 @@ export class Consensus extends React.Component<Consensus.Props, {}> {
smallBlocksRows: 1, smallBlocksRows: 1,
countBlocksInSmallRow: 1, countBlocksInSmallRow: 1,
smallRowsAddFlexClass: false, smallRowsAddFlexClass: false,
lastConsensusInfo: "", lastConsensusInfo: '',
}; };
public shouldComponentUpdate(nextProps: Consensus.Props, nextState: Consensus.State): boolean { public shouldComponentUpdate(
if (this.props.appState.authorities.length === 0 && nextProps.appState.authorities.length === 0) { nextProps: Consensus.Props,
nextState: Consensus.State
): boolean {
if (
this.props.appState.authorities.length === 0 &&
nextProps.appState.authorities.length === 0
) {
return false; return false;
} }
this.calculateBoxCount(false); this.calculateBoxCount(false);
// size detected, but flex class has not yet been added // size detected, but flex class has not yet been added
const largeBlocksSizeDetected = this.largeBlocksSizeDetected(nextState) === true && const largeBlocksSizeDetected =
this.largeBlocksSizeDetected(nextState) === true &&
this.state.largeRowsAddFlexClass === false; this.state.largeRowsAddFlexClass === false;
if (largeBlocksSizeDetected) { if (largeBlocksSizeDetected) {
return true; return true;
} }
const smallBlocksSizeDetected = this.smallBlocksSizeDetected(nextState) === true && const smallBlocksSizeDetected =
this.smallBlocksSizeDetected(nextState) === true &&
this.state.smallRowsAddFlexClass === false; this.state.smallRowsAddFlexClass === false;
if (smallBlocksSizeDetected) { if (smallBlocksSizeDetected) {
return true; return true;
} }
const windowSizeChanged = JSON.stringify(this.state.dimensions) !== const windowSizeChanged =
JSON.stringify(this.state.dimensions) !==
JSON.stringify(nextState.dimensions); JSON.stringify(nextState.dimensions);
if (windowSizeChanged) { if (windowSizeChanged) {
return true; return true;
} }
const newConsensusInfoAvailable = this.state.lastConsensusInfo !== const newConsensusInfoAvailable =
this.state.lastConsensusInfo !==
JSON.stringify(nextProps.appState.consensusInfo); JSON.stringify(nextProps.appState.consensusInfo);
if (newConsensusInfoAvailable) { if (newConsensusInfoAvailable) {
return true; return true;
} }
const authoritySetIdDidChange = this.props.appState.authoritySetId !== const authoritySetIdDidChange =
nextProps.appState.authoritySetId; this.props.appState.authoritySetId !== nextProps.appState.authoritySetId;
if (authoritySetIdDidChange) { if (authoritySetIdDidChange) {
return true; return true;
} }
const authoritiesDidChange = JSON.stringify(this.props.appState.authorities) !== const authoritiesDidChange =
JSON.stringify(this.props.appState.authorities) !==
JSON.stringify(nextProps.appState.authorities); JSON.stringify(nextProps.appState.authorities);
if (authoritiesDidChange) { if (authoritiesDidChange) {
return true; return true;
@@ -123,70 +134,107 @@ export class Consensus extends React.Component<Consensus.Props, {}> {
// if there is more than one block then the size of the first block (with legend) // if there is more than one block then the size of the first block (with legend)
// will be different from the succeeding blocks (without legend) // will be different from the succeeding blocks (without legend)
return state.largeBlockWithLegend.width > -1 && state.largeBlockWithLegend.height > -1 && return (
state.largeBlock.width > -1 && state.largeBlock.height > -1; state.largeBlockWithLegend.width > -1 &&
state.largeBlockWithLegend.height > -1 &&
state.largeBlock.width > -1 &&
state.largeBlock.height > -1
);
} }
public smallBlocksSizeDetected(state: Consensus.State): boolean { public smallBlocksSizeDetected(state: Consensus.State): boolean {
return state.smallBlock.width > -1 && state.largeBlockWithLegend.height > -1; return (
state.smallBlock.width > -1 && state.largeBlockWithLegend.height > -1
);
} }
public calculateBoxCount(wasResized: boolean) { public calculateBoxCount(wasResized: boolean) {
// if the css class for flexing has already been added we don't calculate // if the css class for flexing has already been added we don't calculate
// any box measurements then, because the box sizes would be skewed then. // any box measurements then, because the box sizes would be skewed then.
if ((wasResized || this.state.largeRowsAddFlexClass === false) && this.largeBlocksSizeDetected(this.state)) { if (
(wasResized || this.state.largeRowsAddFlexClass === false) &&
this.largeBlocksSizeDetected(this.state)
) {
// we need to add +2 because of the last block which doesn't contain a border. // we need to add +2 because of the last block which doesn't contain a border.
let countBlocks = (this.state.dimensions.width - this.state.largeBlockWithLegend.width + 2) / let countBlocks =
(this.state.dimensions.width -
this.state.largeBlockWithLegend.width +
2) /
(this.state.largeBlock.width + 2); (this.state.largeBlock.width + 2);
// +1 because the firstRect was subtracted above and needs to be counted back in. // +1 because the firstRect was subtracted above and needs to be counted back in.
// default count is 2 because we need two blocks to measure properly (one with legend // default count is 2 because we need two blocks to measure properly (one with legend
// and one without. these measures are necessary to calculate the number of blocks // and one without. these measures are necessary to calculate the number of blocks
// which fit. // which fit.
countBlocks = Math.floor(countBlocks + 1) < 1 ? 2 : Math.floor(countBlocks + 1); countBlocks =
Math.floor(countBlocks + 1) < 1 ? 2 : Math.floor(countBlocks + 1);
this.setState({largeRowsAddFlexClass: true, countBlocksInLargeRow: countBlocks }); this.setState({
largeRowsAddFlexClass: true,
countBlocksInLargeRow: countBlocks,
});
} }
if ((wasResized || this.state.smallRowsAddFlexClass === false) && this.smallBlocksSizeDetected(this.state)) { if (
(wasResized || this.state.smallRowsAddFlexClass === false) &&
this.smallBlocksSizeDetected(this.state)
) {
const howManyRows = 2; const howManyRows = 2;
const heightLeft = this.state.dimensions.height - (this.state.largeBlock.height * howManyRows); const heightLeft =
this.state.dimensions.height -
this.state.largeBlock.height * howManyRows;
let smallBlocksRows = heightLeft / this.state.smallBlock.height; let smallBlocksRows = heightLeft / this.state.smallBlock.height;
smallBlocksRows = smallBlocksRows < 1 ? 1 : Math.floor(smallBlocksRows); smallBlocksRows = smallBlocksRows < 1 ? 1 : Math.floor(smallBlocksRows);
let countBlocksInSmallRow = this.state.dimensions.width / this.state.smallBlock.width; let countBlocksInSmallRow =
countBlocksInSmallRow = countBlocksInSmallRow < 1 ? 1 : Math.floor(countBlocksInSmallRow); this.state.dimensions.width / this.state.smallBlock.width;
countBlocksInSmallRow =
countBlocksInSmallRow < 1 ? 1 : Math.floor(countBlocksInSmallRow);
this.setState({ smallRowsAddFlexClass: true, countBlocksInSmallRow, smallBlocksRows }); this.setState({
smallRowsAddFlexClass: true,
countBlocksInSmallRow,
smallBlocksRows,
});
} }
} }
public render() { public render() {
this.state.lastConsensusInfo = JSON.stringify(this.props.appState.consensusInfo); this.state.lastConsensusInfo = JSON.stringify(
this.props.appState.consensusInfo
);
const lastBlocks = this.props.appState.consensusInfo; const lastBlocks = this.props.appState.consensusInfo;
if (this.props.appState.authorities.length > VIS_AUTHORITIES_LIMIT) { if (this.props.appState.authorities.length > VIS_AUTHORITIES_LIMIT) {
return <div className="Consensus"> return (
<div className="tooManyAuthorities"> <div className="Consensus">
<p> <div className="tooManyAuthorities">
Too many authorities.</p> <p>Too many authorities.</p>
<p> <p>
Won't display for more than {VIS_AUTHORITIES_LIMIT} authorities Won't display for more than {VIS_AUTHORITIES_LIMIT} authorities to
to protect your browser. protect your browser.
</p> </p>
</div>; </div>
</div>; ;
</div>
);
} }
if (this.props.appState.displayConsensusLoadingScreen && lastBlocks.length < 2) { if (
return <div className="Consensus"> this.props.appState.displayConsensusLoadingScreen &&
<div className="noData"> lastBlocks.length < 2
{lastBlocks.length === 0 ? "No " : "Not yet enough "} ) {
GRANDPA data received by the authorities&hellip; return (
</div>; <div className="Consensus">
</div>; <div className="noData">
{lastBlocks.length === 0 ? 'No ' : 'Not yet enough '}
GRANDPA data received by the authorities&hellip;
</div>
;
</div>
);
} }
let from = 0; let from = 0;
@@ -198,26 +246,29 @@ export class Consensus extends React.Component<Consensus.Props, {}> {
const secondLargeRow = this.getLargeRow(lastBlocks.slice(from, to), 1); const secondLargeRow = this.getLargeRow(lastBlocks.slice(from, to), 1);
from = to; from = to;
to = to + (this.state.smallBlocksRows * this.state.countBlocksInSmallRow); to = to + this.state.smallBlocksRows * this.state.countBlocksInSmallRow;
const smallRow = this.getSmallRow(lastBlocks.slice(from, to)); const smallRow = this.getSmallRow(lastBlocks.slice(from, to));
const get = (measureRef: Maybe<(ref: Element | null) => void>) => const get = (measureRef: Maybe<(ref: Element | null) => void>) => (
<div className="Consensus" ref={measureRef} key="Consensus"> <div className="Consensus" ref={measureRef} key="Consensus">
{firstLargeRow} {firstLargeRow}
{secondLargeRow} {secondLargeRow}
{smallRow} {smallRow}
</div>; </div>
);
if (!(this.state.smallRowsAddFlexClass && this.state.largeRowsAddFlexClass)) { if (
!(this.state.smallRowsAddFlexClass && this.state.largeRowsAddFlexClass)
) {
return ( return (
<React.Fragment> <React.Fragment>
<Measure bounds={true} onResize={this.handleOnResize}> <Measure bounds={true} onResize={this.handleOnResize}>
{({measureRef}) => get(measureRef)} {({ measureRef }) => get(measureRef)}
</Measure> </Measure>
</React.Fragment> </React.Fragment>
); );
} else { } else {
return (get(null)); return get(null);
} }
} }
@@ -232,91 +283,132 @@ export class Consensus extends React.Component<Consensus.Props, {}> {
return []; return [];
} }
return this.props.appState.authorities.map(address => { return this.props.appState.authorities.map((address) => {
const node2 = this.props.appState.nodes.sorted().filter(node => node.validator === address)[0]; const node2 = this.props.appState.nodes
.sorted()
.filter((node) => node.validator === address)[0];
if (!node2) { if (!node2) {
return {Address: address, NodeId: null, Name: null} as Types.Authority; return {
Address: address,
NodeId: null,
Name: null,
} as Types.Authority;
} }
return {Address: address, NodeId: node2.id, Name: node2.name} as Types.Authority; return {
Address: address,
NodeId: node2.id,
Name: node2.name,
} as Types.Authority;
}); });
} }
private getLargeRow(blocks: Types.ConsensusInfo, id: number) { private getLargeRow(blocks: Types.ConsensusInfo, id: number) {
const largeBlockSizeChanged = (isFirstBlock: boolean, rect: BoundingRect) => { const largeBlockSizeChanged = (
isFirstBlock: boolean,
rect: BoundingRect
) => {
if (this.largeBlocksSizeDetected(this.state)) { if (this.largeBlocksSizeDetected(this.state)) {
return; return;
} }
if (isFirstBlock) { if (isFirstBlock) {
this.setState({largeBlockWithLegend: {width: rect.width, height: rect.height} }); this.setState({
largeBlockWithLegend: { width: rect.width, height: rect.height },
});
} else { } else {
this.setState({largeBlock: {width: rect.width, height: rect.height} }); this.setState({
largeBlock: { width: rect.width, height: rect.height },
});
} }
}; };
const stretchLastRowMajor = blocks.length < this.state.countBlocksInLargeRow ? const stretchLastRowMajor =
'noStretchOnLastRow' : ''; blocks.length < this.state.countBlocksInLargeRow
const flexClass = this.state.largeRowsAddFlexClass ? 'flexContainerLargeRow' : ''; ? 'noStretchOnLastRow'
: '';
const flexClass = this.state.largeRowsAddFlexClass
? 'flexContainerLargeRow'
: '';
return <div return (
<div
className={`ConsensusList LargeRow ${flexClass} ${stretchLastRowMajor}`} className={`ConsensusList LargeRow ${flexClass} ${stretchLastRowMajor}`}
key={`consensusList_${id}`}> key={`consensusList_${id}`}
>
{blocks.map((item, i) => { {blocks.map((item, i) => {
const [height, consensusView] = item; const [height, consensusView] = item;
return <ConsensusBlock return (
changeBlocks={largeBlockSizeChanged} <ConsensusBlock
firstInRow={i === 0} changeBlocks={largeBlockSizeChanged}
lastInRow={false} firstInRow={i === 0}
compact={false} lastInRow={false}
key={height} compact={false}
height={height} key={height}
measure={!this.state.largeRowsAddFlexClass} height={height}
consensusView={consensusView} measure={!this.state.largeRowsAddFlexClass}
authorities={this.getAuthorities()} consensusView={consensusView}
authoritySetId={this.props.appState.authoritySetId} authorities={this.getAuthorities()}
/>; authoritySetId={this.props.appState.authoritySetId}
/>
);
})} })}
</div>; </div>
);
} }
private getSmallRow(blocks: Types.ConsensusInfo) { private getSmallRow(blocks: Types.ConsensusInfo) {
const smallBlockSizeChanged = (isFirstBlock: boolean, rect: BoundingRect) => { const smallBlockSizeChanged = (
isFirstBlock: boolean,
rect: BoundingRect
) => {
if (this.smallBlocksSizeDetected(this.state)) { if (this.smallBlocksSizeDetected(this.state)) {
return; return;
} }
const dimensionsChanged = this.state.smallBlock.height !== rect.height && const dimensionsChanged =
this.state.smallBlock.height !== rect.height &&
this.state.smallBlock.width !== rect.width; this.state.smallBlock.width !== rect.width;
if (dimensionsChanged) { if (dimensionsChanged) {
this.setState({ smallBlock: {width: rect.width, height: rect.height} }); this.setState({
smallBlock: { width: rect.width, height: rect.height },
});
} }
}; };
const stretchLastRow = const stretchLastRow =
blocks.length < this.state.countBlocksInSmallRow * this.state.smallBlocksRows ? blocks.length <
'noStretchOnLastRow' : ''; this.state.countBlocksInSmallRow * this.state.smallBlocksRows
const classes = `ConsensusList SmallRow ${this.state.smallRowsAddFlexClass ? 'flexContainerSmallRow' : ''} ${stretchLastRow}`; ? 'noStretchOnLastRow'
: '';
const classes = `ConsensusList SmallRow ${
this.state.smallRowsAddFlexClass ? 'flexContainerSmallRow' : ''
} ${stretchLastRow}`;
return <div className={classes} key="smallRow"> return (
{blocks.map((item, i) => { <div className={classes} key="smallRow">
const [height, consensusView] = item; {blocks.map((item, i) => {
let lastInRow = (i+1) % this.state.countBlocksInSmallRow === 0 ? true : false; const [height, consensusView] = item;
if (lastInRow && i === 0) { let lastInRow =
// should not be marked as last one in row if it's the very first in row (i + 1) % this.state.countBlocksInSmallRow === 0 ? true : false;
lastInRow = false; if (lastInRow && i === 0) {
} // should not be marked as last one in row if it's the very first in row
lastInRow = false;
}
return <ConsensusBlock return (
changeBlocks={smallBlockSizeChanged} <ConsensusBlock
firstInRow={i === 0} changeBlocks={smallBlockSizeChanged}
lastInRow={lastInRow} firstInRow={i === 0}
compact={true} lastInRow={lastInRow}
key={height} compact={true}
height={height} key={height}
measure={!this.state.smallRowsAddFlexClass} height={height}
consensusView={consensusView} measure={!this.state.smallRowsAddFlexClass}
authorities={this.getAuthorities()} consensusView={consensusView}
authoritySetId={this.props.appState.authoritySetId} />; authorities={this.getAuthorities()}
}) authoritySetId={this.props.appState.authoritySetId}
} />
</div>; );
})}
</div>
);
} }
private async subscribeConsensus(chain: Types.ChainLabel) { private async subscribeConsensus(chain: Types.ChainLabel) {
@@ -21,7 +21,8 @@
border-bottom: 1px dashed #999; border-bottom: 1px dashed #999;
} }
.Consensus .finalizedInfo, .legend { .Consensus .finalizedInfo,
.legend {
border-bottom: 1px dotted #555555; border-bottom: 1px dotted #555555;
} }
@@ -89,7 +90,7 @@
} }
.Consensus .explicit { .Consensus .explicit {
fill: #E70E81; fill: #e70e81;
} }
.Consensus .nodeName { .Consensus .nodeName {
@@ -134,7 +135,8 @@
cursor: pointer; cursor: pointer;
} }
.Consensus .Row th, .Consensus .Row td { .Consensus .Row th,
.Consensus .Row td {
text-align: left; text-align: left;
padding: 2px; padding: 2px;
} }
@@ -199,4 +201,3 @@
.Consensus .even { .Consensus .even {
background-color: #333; background-color: #333;
} }
@@ -1,6 +1,6 @@
import * as React from 'react'; import * as React from 'react';
import Measure, {BoundingRect, ContentRect} from 'react-measure'; import Measure, { BoundingRect, ContentRect } from 'react-measure';
import { Types, Maybe } from '@dotstats/common'; import { Types, Maybe } from '@dotstats/common';
import { Icon, Tooltip, PolkadotIcon } from '../'; import { Icon, Tooltip, PolkadotIcon } from '../';
@@ -28,15 +28,19 @@ export namespace ConsensusBlock {
export class ConsensusBlock extends React.Component<ConsensusBlock.Props, {}> { export class ConsensusBlock extends React.Component<ConsensusBlock.Props, {}> {
public state = { public state = {
lastConsensusView: "", lastConsensusView: '',
}; };
public shouldComponentUpdate(nextProps: ConsensusBlock.Props): boolean { public shouldComponentUpdate(nextProps: ConsensusBlock.Props): boolean {
if (this.props.authorities.length === 0 && nextProps.authorities.length === 0) { if (
this.props.authorities.length === 0 &&
nextProps.authorities.length === 0
) {
return false; return false;
} }
const positionInfoChanged = this.props.firstInRow !== nextProps.firstInRow || const positionInfoChanged =
this.props.firstInRow !== nextProps.firstInRow ||
this.props.lastInRow !== nextProps.lastInRow; this.props.lastInRow !== nextProps.lastInRow;
if (positionInfoChanged) { if (positionInfoChanged) {
return true; return true;
@@ -53,75 +57,104 @@ export class ConsensusBlock extends React.Component<ConsensusBlock.Props, {}> {
public render() { public render() {
this.state.lastConsensusView = JSON.stringify(this.props.consensusView); this.state.lastConsensusView = JSON.stringify(this.props.consensusView);
const finalizedByWhom = this.props.authorities.filter(authority => this.isFinalized(authority)); const finalizedByWhom = this.props.authorities.filter((authority) =>
this.isFinalized(authority)
);
const ratio = finalizedByWhom.length + '/' + this.props.authorities.length; const ratio = finalizedByWhom.length + '/' + this.props.authorities.length;
let titleFinal = <span>{ratio}</span>; let titleFinal = <span>{ratio}</span>;
const majorityFinalized = finalizedByWhom.length / this.props.authorities.length >= 2/3; const majorityFinalized =
finalizedByWhom.length / this.props.authorities.length >= 2 / 3;
if (majorityFinalized && !this.props.compact) { if (majorityFinalized && !this.props.compact) {
titleFinal = <span>FINAL</span>; titleFinal = <span>FINAL</span>;
} else if (majorityFinalized && this.props.compact) { } else if (majorityFinalized && this.props.compact) {
const hash = this.getFinalizedHash(finalizedByWhom[0]); const hash = this.getFinalizedHash(finalizedByWhom[0]);
titleFinal = <Jdenticon hash={hash ? String(hash) : ''} size={this.props.compact ? '14px' : '28px'}/> titleFinal = (
<Jdenticon
hash={hash ? String(hash) : ''}
size={this.props.compact ? '14px' : '28px'}
/>
);
} }
const handleOnResize = (contentRect: ContentRect) => { const handleOnResize = (contentRect: ContentRect) => {
this.props.changeBlocks(this.props.firstInRow, contentRect.bounds as BoundingRect); this.props.changeBlocks(
this.props.firstInRow,
contentRect.bounds as BoundingRect
);
}; };
const get = (measureRef: Maybe<(ref: Element | null) => void>) => { const get = (measureRef: Maybe<(ref: Element | null) => void>) => {
return <div return (
className={ <div
`BlockConsensusMatrice className={`BlockConsensusMatrice
${this.props.firstInRow ? 'firstInRow' : ''} ${this.props.lastInRow ? 'lastInRow' : ''}` ${this.props.firstInRow ? 'firstInRow' : ''} ${
} this.props.lastInRow ? 'lastInRow' : ''
key={'block_' + this.props.height}> }`}
<table ref={measureRef} key={'block_table_' + this.props.height}> key={'block_' + this.props.height}
<thead key={'block_thead_' + this.props.height}> >
<tr className="Row" key={'block_row_' + this.props.height}> <table ref={measureRef} key={'block_table_' + this.props.height}>
{this.props.firstInRow && !this.props.compact ? <thead key={'block_thead_' + this.props.height}>
<th className="emptylegend" key={'block_row_' + this.props.height + '_empty'}>&nbsp;</th> : null} <tr className="Row" key={'block_row_' + this.props.height}>
<th className="legend" key={'block_row_' + this.props.height + '_legend'}> {this.props.firstInRow && !this.props.compact ? (
<Tooltip text={`Block number: ${this.props.height}`}> <th
{this.displayBlockNumber()} className="emptylegend"
</Tooltip> key={'block_row_' + this.props.height + '_empty'}
</th> >
<th className='finalizedInfo' key={'block_row_' + this.props.height + '_finalized_info'}> &nbsp;
{titleFinal} </th>
</th> ) : null}
{this.props.authorities.map(authority => <th
<th className="legend"
className="matrixXLegend" key={'block_row_' + this.props.height + '_legend'}
key={`${this.props.height}_matrice_x_${authority.Address}`}> >
{this.getAuthorityContent(authority)} <Tooltip text={`Block number: ${this.props.height}`}>
</th>)} {this.displayBlockNumber()}
</tr> </Tooltip>
</thead> </th>
<tbody key={'block_row_' + this.props.height + '_tbody'}> <th
{this.props.authorities.map((authority, row) => className="finalizedInfo"
this.renderMatriceRow(authority, this.props.authorities, row))} key={'block_row_' + this.props.height + '_finalized_info'}
</tbody> >
</table> {titleFinal}
</div> </th>
{this.props.authorities.map((authority) => (
<th
className="matrixXLegend"
key={`${this.props.height}_matrice_x_${authority.Address}`}
>
{this.getAuthorityContent(authority)}
</th>
))}
</tr>
</thead>
<tbody key={'block_row_' + this.props.height + '_tbody'}>
{this.props.authorities.map((authority, row) =>
this.renderMatriceRow(authority, this.props.authorities, row)
)}
</tbody>
</table>
</div>
);
}; };
if (this.props.measure) { if (this.props.measure) {
return ( return (
<Measure bounds={true} onResize={handleOnResize}>{({measureRef}) => ( <Measure bounds={true} onResize={handleOnResize}>
get(measureRef) {({ measureRef }) => get(measureRef)}
)} </Measure>
</Measure>
); );
} else { } else {
return (get(null)); return get(null);
} }
} }
private displayBlockNumber(): string { private displayBlockNumber(): string {
const blockNumber = String(this.props.height); const blockNumber = String(this.props.height);
return blockNumber.length > 2 ? return blockNumber.length > 2
'…' + blockNumber.substr(blockNumber.length - 2, blockNumber.length) : blockNumber; ? '…' + blockNumber.substr(blockNumber.length - 2, blockNumber.length)
: blockNumber;
} }
private isFinalized(authority: Types.Authority): boolean { private isFinalized(authority: Types.Authority): boolean {
@@ -132,8 +165,12 @@ export class ConsensusBlock extends React.Component<ConsensusBlock.Props, {}> {
const { Address: addr } = authority; const { Address: addr } = authority;
const consensus = this.props.consensusView; const consensus = this.props.consensusView;
return consensus != null && addr in consensus && addr in consensus[addr] return (
&& consensus[addr][addr].Finalized === true; consensus != null &&
addr in consensus &&
addr in consensus[addr] &&
consensus[addr][addr].Finalized === true
);
} }
private getFinalizedHash(authority: Types.Authority): Maybe<Types.BlockHash> { private getFinalizedHash(authority: Types.Authority): Maybe<Types.BlockHash> {
@@ -144,55 +181,114 @@ export class ConsensusBlock extends React.Component<ConsensusBlock.Props, {}> {
return null; return null;
} }
private renderMatriceRow(authority: Types.Authority, authorities: Types.Authority[], row: number): JSX.Element { private renderMatriceRow(
authority: Types.Authority,
authorities: Types.Authority[],
row: number
): JSX.Element {
let finalizedInfo = <span>&nbsp;</span>; let finalizedInfo = <span>&nbsp;</span>;
let finalizedHash; let finalizedHash;
if (authority.NodeId != null && this.isFinalized(authority)) { if (authority.NodeId != null && this.isFinalized(authority)) {
const matrice = this.props.consensusView[authority.Address][authority.Address]; const matrice = this.props.consensusView[authority.Address][
authority.Address
];
finalizedInfo = matrice.ImplicitFinalized ? finalizedInfo = matrice.ImplicitFinalized ? (
<Icon className="implicit" src={finalizedIcon} alt="" /> : <Icon className="implicit" src={finalizedIcon} alt="" />
<Icon className="explicit" src={finalizedIcon} alt="" />; ) : (
<Icon className="explicit" src={finalizedIcon} alt="" />
);
finalizedHash = matrice.FinalizedHash ? finalizedHash = matrice.FinalizedHash ? (
<Jdenticon hash={matrice.FinalizedHash} size="28px"/> : <Jdenticon hash={matrice.FinalizedHash} size="28px" />
<div className="jdenticonPlaceholder">&nbsp;</div>; ) : (
<div className="jdenticonPlaceholder">&nbsp;</div>
);
} }
const name = authority.Name ? const name = authority.Name ? (
<span>{authority.Name}</span> : <em>no data received from node</em>; <span>{authority.Name}</span>
const firstName = this.props.firstInRow ? ) : (
<td key={"name_" + name} className="nameLegend">{name}</td> : ''; <em>no data received from node</em>
);
const firstName = this.props.firstInRow ? (
<td key={'name_' + name} className="nameLegend">
{name}
</td>
) : (
''
);
return <tr className="Row" key={'block_row_' + this.props.height + '_' + row}> return (
{firstName} <tr className="Row" key={'block_row_' + this.props.height + '_' + row}>
<td className="legend" key={'block_row_' + this.props.height + '_' + row + '_legend'}>{this.getAuthorityContent(authority)}</td> {firstName}
<td className="finalizedInfo" key={'block_row_' + this.props.height + '_' + row + '_finalizedInfo'}>{finalizedInfo}{finalizedHash}</td> <td
{ className="legend"
authorities.map((columnNode, column) => { key={'block_row_' + this.props.height + '_' + row + '_legend'}
>
{this.getAuthorityContent(authority)}
</td>
<td
className="finalizedInfo"
key={'block_row_' + this.props.height + '_' + row + '_finalizedInfo'}
>
{finalizedInfo}
{finalizedHash}
</td>
{authorities.map((columnNode, column) => {
const evenOdd = ((row % 2) + column) % 2 === 0 ? 'even' : 'odd'; const evenOdd = ((row % 2) + column) % 2 === 0 ? 'even' : 'odd';
return <td key={'matrice_' + this.props.height + '_' + row + '_' + authority.Address + '_' + columnNode.Address} return (
className={`matrice ${evenOdd}`}>{this.getCellContent(authority, columnNode)}</td> <td
}) key={
} 'matrice_' +
</tr>; this.props.height +
'_' +
row +
'_' +
authority.Address +
'_' +
columnNode.Address
}
className={`matrice ${evenOdd}`}
>
{this.getCellContent(authority, columnNode)}
</td>
);
})}
</tr>
);
} }
private getAuthorityContent(authority: Types.Authority): JSX.Element { private getAuthorityContent(authority: Types.Authority): JSX.Element {
return <div className="nodeContent" key={'authority_' + this.props.height + '_' + authority.Address}> return (
<div className="nodeAddress" key={'authority_' + authority.Address}> <div
<PolkadotIcon account={authority.Address} size={this.props.compact ? 14 : 28} /> className="nodeContent"
key={'authority_' + this.props.height + '_' + authority.Address}
>
<div className="nodeAddress" key={'authority_' + authority.Address}>
<PolkadotIcon
account={authority.Address}
size={this.props.compact ? 14 : 28}
/>
</div>
</div> </div>
</div>; );
} }
private getCellContent(rowAuthority: Types.Authority, columnAuthority: Types.Authority) { private getCellContent(
const consensusInfo = this.props.consensusView && rowAuthority: Types.Authority,
columnAuthority: Types.Authority
) {
const consensusInfo =
this.props.consensusView &&
rowAuthority.Address && rowAuthority.Address &&
rowAuthority.Address in this.props.consensusView && rowAuthority.Address in this.props.consensusView &&
columnAuthority.Address in this.props.consensusView[rowAuthority.Address] ? columnAuthority.Address in this.props.consensusView[rowAuthority.Address]
this.props.consensusView[rowAuthority.Address][columnAuthority.Address] : null; ? this.props.consensusView[rowAuthority.Address][
columnAuthority.Address
]
: null;
const prevote = consensusInfo && consensusInfo.Prevote; const prevote = consensusInfo && consensusInfo.Prevote;
const implicitPrevote = consensusInfo && consensusInfo.ImplicitPrevote; const implicitPrevote = consensusInfo && consensusInfo.ImplicitPrevote;
@@ -205,23 +301,35 @@ export class ConsensusBlock extends React.Component<ConsensusBlock.Props, {}> {
let statPrecommit; let statPrecommit;
if (implicitPrevote) { if (implicitPrevote) {
statPrevote = <Icon src={checkIcon} className="implicit" alt="Implicit Prevote"/>; statPrevote = (
<Icon src={checkIcon} className="implicit" alt="Implicit Prevote" />
);
} }
if (implicitPrecommit) { if (implicitPrecommit) {
statPrecommit = <Icon src={checkIcon} className="implicit" alt="Implicit Precommit"/>; statPrecommit = (
<Icon src={checkIcon} className="implicit" alt="Implicit Precommit" />
);
} }
if (prevote) { if (prevote) {
statPrevote = <Icon src={checkIcon} className="explicit" alt="Prevote"/>; statPrevote = (
<Icon src={checkIcon} className="explicit" alt="Prevote" />
);
} }
if (precommit) { if (precommit) {
statPrecommit = <Icon src={checkIcon} className="explicit" alt="Precommit"/>; statPrecommit = (
<Icon src={checkIcon} className="explicit" alt="Precommit" />
);
} }
return <span key={"icons_pre"}>{statPrevote}{statPrecommit}</span>; return (
<span key={'icons_pre'}>
{statPrevote}
{statPrecommit}
</span>
);
} else { } else {
return <Icon src={hatchingIcon} className="hatching" alt=""/>; return <Icon src={hatchingIcon} className="hatching" alt="" />;
} }
} }
} }
@@ -3,8 +3,8 @@ import * as React from 'react';
import './Jdenticon.css'; import './Jdenticon.css';
export interface Props { export interface Props {
hash: string, hash: string;
size: string size: string;
} }
class Jdenticon extends React.Component<Props, {}> { class Jdenticon extends React.Component<Props, {}> {
@@ -26,13 +26,15 @@ class Jdenticon extends React.Component<Props, {}> {
public render() { public render() {
const { hash, size } = this.props; const { hash, size } = this.props;
return <svg return (
className="Jdenticon" <svg
ref={element => this.handleRef(element)} className="Jdenticon"
width={size} ref={(element) => this.handleRef(element)}
height={size} width={size}
data-jdenticon-value={hash} height={size}
/>; data-jdenticon-value={hash}
/>
);
} }
private handleRef(element: any) { private handleRef(element: any) {
+1 -1
View File
@@ -10,7 +10,7 @@
border-radius: 4px; border-radius: 4px;
background: #111; background: #111;
color: #fff; color: #fff;
box-shadow: 0 2px 10px rgba(0,0,0,0.5); box-shadow: 0 2px 10px rgba(0, 0, 0, 0.5);
} }
.Filter-hidden { .Filter-hidden {
+24 -12
View File
@@ -21,7 +21,7 @@ const ESCAPE_KEY = 27;
export class Filter extends React.Component<Filter.Props, {}> { export class Filter extends React.Component<Filter.Props, {}> {
public state = { public state = {
value: '' value: '',
}; };
private filterInput: HTMLInputElement; private filterInput: HTMLInputElement;
@@ -34,23 +34,34 @@ export class Filter extends React.Component<Filter.Props, {}> {
window.removeEventListener('keyup', this.onWindowKeyUp); window.removeEventListener('keyup', this.onWindowKeyUp);
} }
public shouldComponentUpdate(nextProps: Filter.Props, nextState: Filter.State): boolean { public shouldComponentUpdate(
return this.props.onChange !== nextProps.onChange || this.state.value !== nextState.value; nextProps: Filter.Props,
nextState: Filter.State
): boolean {
return (
this.props.onChange !== nextProps.onChange ||
this.state.value !== nextState.value
);
} }
public render() { public render() {
const { value } = this.state; const { value } = this.state;
let className = "Filter"; let className = 'Filter';
if (value === '') { if (value === '') {
className += " Filter-hidden"; className += ' Filter-hidden';
} }
return ( return (
<div className={className}> <div className={className}>
<Icon src={searchIcon} /> <Icon src={searchIcon} />
<input ref={this.onRef} value={value} onChange={this.onChange} onKeyUp={this.onKeyUp} /> <input
ref={this.onRef}
value={value}
onChange={this.onChange}
onKeyUp={this.onKeyUp}
/>
</div> </div>
); );
} }
@@ -63,11 +74,11 @@ export class Filter extends React.Component<Filter.Props, {}> {
private onRef = (el: HTMLInputElement) => { private onRef = (el: HTMLInputElement) => {
this.filterInput = el; this.filterInput = el;
} };
private onChange = () => { private onChange = () => {
this.setValue(this.filterInput.value); this.setValue(this.filterInput.value);
} };
private onKeyUp = (event: React.KeyboardEvent<HTMLInputElement>) => { private onKeyUp = (event: React.KeyboardEvent<HTMLInputElement>) => {
event.stopPropagation(); event.stopPropagation();
@@ -75,7 +86,7 @@ export class Filter extends React.Component<Filter.Props, {}> {
if (event.keyCode === ESCAPE_KEY) { if (event.keyCode === ESCAPE_KEY) {
this.setValue(''); this.setValue('');
} }
} };
private onWindowKeyUp = (event: KeyboardEvent) => { private onWindowKeyUp = (event: KeyboardEvent) => {
if (event.ctrlKey) { if (event.ctrlKey) {
@@ -94,7 +105,7 @@ export class Filter extends React.Component<Filter.Props, {}> {
this.setValue(key); this.setValue(key);
this.filterInput.focus(); this.filterInput.focus();
} }
} };
private getNodeFilter(value: string): Maybe<(node: Node) => boolean> { private getNodeFilter(value: string): Maybe<(node: Node) => boolean> {
if (value === '') { if (value === '') {
@@ -105,9 +116,10 @@ export class Filter extends React.Component<Filter.Props, {}> {
return ({ name, city }) => { return ({ name, city }) => {
const matchesName = name.toLowerCase().indexOf(filter) !== -1; const matchesName = name.toLowerCase().indexOf(filter) !== -1;
const matchesCity = city != null && city.toLowerCase().indexOf(filter) !== -1; const matchesCity =
city != null && city.toLowerCase().indexOf(filter) !== -1;
return matchesName || matchesCity; return matchesName || matchesCity;
} };
} }
} }
+9 -9
View File
@@ -1,14 +1,14 @@
.Icon { .Icon {
fill: currentColor; fill: currentColor;
height: 1em; height: 1em;
width: 1em; width: 1em;
text-align: center; text-align: center;
line-height: 1em; line-height: 1em;
vertical-align: middle; vertical-align: middle;
display: inline-block; display: inline-block;
} }
.Icon svg { .Icon svg {
width: auto; width: auto;
height: 1em; height: 1em;
} }
+15 -5
View File
@@ -7,20 +7,30 @@ export interface Props {
alt?: string; alt?: string;
className?: string; className?: string;
onClick?: () => void; onClick?: () => void;
}; }
export class Icon extends React.Component<{}, Props> { export class Icon extends React.Component<{}, Props> {
public props: Props; public props: Props;
public shouldComponentUpdate(nextProps: Props) { public shouldComponentUpdate(nextProps: Props) {
return this.props.src !== nextProps.src return (
|| this.props.alt !== nextProps.alt this.props.src !== nextProps.src ||
|| this.props.className !== nextProps.className; this.props.alt !== nextProps.alt ||
this.props.className !== nextProps.className
);
} }
public render() { public render() {
const { alt, className, onClick, src } = this.props; const { alt, className, onClick, src } = this.props;
return <ReactSVG key={this.props.src} title={alt} className={`Icon ${ className || '' }`} path={src} onClick={onClick} />; return (
<ReactSVG
key={this.props.src}
title={alt}
className={`Icon ${className || ''}`}
path={src}
onClick={onClick}
/>
);
} }
} }
+157 -65
View File
@@ -3,7 +3,12 @@ import { Types, Maybe, timestamp } from '@dotstats/common';
import { State, Node } from '../../state'; import { State, Node } from '../../state';
import { Truncate } from './'; import { Truncate } from './';
import { Ago, Icon, Tooltip, Sparkline, PolkadotIcon } from '../'; import { Ago, Icon, Tooltip, Sparkline, PolkadotIcon } from '../';
import { formatNumber, getHashData, milliOrSecond, secondsWithPrecision } from '../../utils'; import {
formatNumber,
getHashData,
milliOrSecond,
secondsWithPrecision,
} from '../../utils';
export interface Column { export interface Column {
label: string; label: string;
@@ -64,17 +69,17 @@ const ICONS = {
'edgeware-node': edgewareIcon, 'edgeware-node': edgewareIcon,
'Edgeware Node': edgewareIcon, 'Edgeware Node': edgewareIcon,
'joystream-node': joystreamIcon, 'joystream-node': joystreamIcon,
'ChainX': chainXIcon, ChainX: chainXIcon,
'ladder-node': ladderIcon, 'ladder-node': ladderIcon,
'cennznet-node': cennznetIcon, 'cennznet-node': cennznetIcon,
'Darwinia': darwiniaIcon, Darwinia: darwiniaIcon,
'Darwinia Testnet': darwiniaIcon, 'Darwinia Testnet': darwiniaIcon,
'turing-node': turingIcon, 'turing-node': turingIcon,
'dothereum': dothereumIcon, dothereum: dothereumIcon,
'katalchain': katalchainIcon, katalchain: katalchainIcon,
'bifrost-node': bifrostIcon, 'bifrost-node': bifrostIcon,
'totem-meccano-node': totemIcon, 'totem-meccano-node': totemIcon,
'Totem': totemIcon, Totem: totemIcon,
}; };
export namespace Column { export namespace Column {
@@ -82,7 +87,7 @@ export namespace Column {
label: 'Node', label: 'Node',
icon: nodeIcon, icon: nodeIcon,
sortBy: ({ sortableName }) => sortableName, sortBy: ({ sortableName }) => sortableName,
render: ({ name }) => <Truncate text={name} position="left" /> render: ({ name }) => <Truncate text={name} position="left" />,
}; };
export const VALIDATOR: Column = { export const VALIDATOR: Column = {
@@ -92,8 +97,16 @@ export namespace Column {
setting: 'validator', setting: 'validator',
sortBy: ({ validator }) => validator || '', sortBy: ({ validator }) => validator || '',
render: ({ validator }) => { render: ({ validator }) => {
return validator ? <Tooltip text={validator} copy={true}><span className="Row-validator"><PolkadotIcon account={validator} size={16} /></span></Tooltip> : '-'; return validator ? (
} <Tooltip text={validator} copy={true}>
<span className="Row-validator">
<PolkadotIcon account={validator} size={16} />
</span>
</Tooltip>
) : (
'-'
);
},
}; };
export const LOCATION: Column = { export const LOCATION: Column = {
@@ -102,7 +115,8 @@ export namespace Column {
width: 140, width: 140,
setting: 'location', setting: 'location',
sortBy: ({ city }) => city || '', sortBy: ({ city }) => city || '',
render: ({ city }) => city ? <Truncate position="left" text={city} /> : '-' render: ({ city }) =>
city ? <Truncate position="left" text={city} /> : '-',
}; };
export const IMPLEMENTATION: Column = { export const IMPLEMENTATION: Column = {
@@ -120,7 +134,7 @@ export namespace Column {
<Icon src={implIcon} /> {semver} <Icon src={implIcon} /> {semver}
</Tooltip> </Tooltip>
); );
} },
}; };
export const NETWORK_ID: Column = { export const NETWORK_ID: Column = {
@@ -129,7 +143,8 @@ export namespace Column {
width: 90, width: 90,
setting: 'networkId', setting: 'networkId',
sortBy: ({ networkId }) => networkId || '', sortBy: ({ networkId }) => networkId || '',
render: ({ networkId }) => networkId ? <Truncate position="left" text={networkId} /> : '-' render: ({ networkId }) =>
networkId ? <Truncate position="left" text={networkId} /> : '-',
}; };
export const PEERS: Column = { export const PEERS: Column = {
@@ -138,7 +153,7 @@ export namespace Column {
width: 26, width: 26,
setting: 'peers', setting: 'peers',
sortBy: ({ peers }) => peers, sortBy: ({ peers }) => peers,
render: ({ peers }) => `${peers}` render: ({ peers }) => `${peers}`,
}; };
export const TXS: Column = { export const TXS: Column = {
@@ -147,7 +162,7 @@ export namespace Column {
width: 26, width: 26,
setting: 'txs', setting: 'txs',
sortBy: ({ txs }) => txs, sortBy: ({ txs }) => txs,
render: ({ txs }) => `${txs}` render: ({ txs }) => `${txs}`,
}; };
export const CPU: Column = { export const CPU: Column = {
@@ -155,16 +170,24 @@ export namespace Column {
icon: cpuIcon, icon: cpuIcon,
width: 40, width: 40,
setting: 'cpu', setting: 'cpu',
sortBy: ({ cpu }) => cpu.length < 3 ? 0 : cpu[cpu.length - 1], sortBy: ({ cpu }) => (cpu.length < 3 ? 0 : cpu[cpu.length - 1]),
render: ({ cpu, chartstamps }) => { render: ({ cpu, chartstamps }) => {
if (cpu.length < 3) { if (cpu.length < 3) {
return '-'; return '-';
} }
return ( return (
<Sparkline width={44} height={16} stroke={1} format={formatCPU} values={cpu} stamps={chartstamps} minScale={100} /> <Sparkline
width={44}
height={16}
stroke={1}
format={formatCPU}
values={cpu}
stamps={chartstamps}
minScale={100}
/>
); );
} },
}; };
export const MEM: Column = { export const MEM: Column = {
@@ -172,16 +195,24 @@ export namespace Column {
icon: memoryIcon, icon: memoryIcon,
width: 40, width: 40,
setting: 'mem', setting: 'mem',
sortBy: ({ mem }) => mem.length < 3 ? 0 : mem[mem.length - 1], sortBy: ({ mem }) => (mem.length < 3 ? 0 : mem[mem.length - 1]),
render: ({ mem, chartstamps }) => { render: ({ mem, chartstamps }) => {
if (mem.length < 3) { if (mem.length < 3) {
return '-'; return '-';
} }
return ( return (
<Sparkline width={44} height={16} stroke={1} format={formatMemory} values={mem} stamps={chartstamps} minScale={MEMORY_SCALE} /> <Sparkline
width={44}
height={16}
stroke={1}
format={formatMemory}
values={mem}
stamps={chartstamps}
minScale={MEMORY_SCALE}
/>
); );
} },
}; };
export const UPLOAD: Column = { export const UPLOAD: Column = {
@@ -189,16 +220,24 @@ export namespace Column {
icon: uploadIcon, icon: uploadIcon,
width: 40, width: 40,
setting: 'upload', setting: 'upload',
sortBy: ({ upload }) => upload.length < 3 ? 0 : upload[upload.length - 1], sortBy: ({ upload }) => (upload.length < 3 ? 0 : upload[upload.length - 1]),
render: ({ upload, chartstamps }) => { render: ({ upload, chartstamps }) => {
if (upload.length < 3) { if (upload.length < 3) {
return '-'; return '-';
} }
return ( return (
<Sparkline width={44} height={16} stroke={1} format={formatBandwidth} values={upload} stamps={chartstamps} minScale={BANDWIDTH_SCALE} /> <Sparkline
width={44}
height={16}
stroke={1}
format={formatBandwidth}
values={upload}
stamps={chartstamps}
minScale={BANDWIDTH_SCALE}
/>
); );
} },
}; };
export const DOWNLOAD: Column = { export const DOWNLOAD: Column = {
@@ -206,16 +245,25 @@ export namespace Column {
icon: downloadIcon, icon: downloadIcon,
width: 40, width: 40,
setting: 'download', setting: 'download',
sortBy: ({ download }) => download.length < 3 ? 0 : download[download.length - 1], sortBy: ({ download }) =>
download.length < 3 ? 0 : download[download.length - 1],
render: ({ download, chartstamps }) => { render: ({ download, chartstamps }) => {
if (download.length < 3) { if (download.length < 3) {
return '-'; return '-';
} }
return ( return (
<Sparkline width={44} height={16} stroke={1} format={formatBandwidth} values={download} stamps={chartstamps} minScale={BANDWIDTH_SCALE} /> <Sparkline
width={44}
height={16}
stroke={1}
format={formatBandwidth}
values={download}
stamps={chartstamps}
minScale={BANDWIDTH_SCALE}
/>
); );
} },
}; };
export const STATE_CACHE: Column = { export const STATE_CACHE: Column = {
@@ -223,16 +271,25 @@ export namespace Column {
icon: stateIcon, icon: stateIcon,
width: 40, width: 40,
setting: 'stateCacheSize', setting: 'stateCacheSize',
sortBy: ({ stateCacheSize }) => stateCacheSize.length < 3 ? 0 : stateCacheSize[stateCacheSize.length - 1], sortBy: ({ stateCacheSize }) =>
stateCacheSize.length < 3 ? 0 : stateCacheSize[stateCacheSize.length - 1],
render: ({ stateCacheSize, chartstamps }) => { render: ({ stateCacheSize, chartstamps }) => {
if (stateCacheSize.length < 3) { if (stateCacheSize.length < 3) {
return '-'; return '-';
} }
return ( return (
<Sparkline width={44} height={16} stroke={1} format={formatBytes} values={stateCacheSize} stamps={chartstamps} minScale={MEMORY_SCALE} /> <Sparkline
width={44}
height={16}
stroke={1}
format={formatBytes}
values={stateCacheSize}
stamps={chartstamps}
minScale={MEMORY_SCALE}
/>
); );
} },
}; };
export const DB_CACHE: Column = { export const DB_CACHE: Column = {
@@ -240,16 +297,25 @@ export namespace Column {
icon: databaseIcon, icon: databaseIcon,
width: 40, width: 40,
setting: 'dbCacheSize', setting: 'dbCacheSize',
sortBy: ({ dbCacheSize }) => dbCacheSize.length < 3 ? 0 : dbCacheSize[dbCacheSize.length - 1], sortBy: ({ dbCacheSize }) =>
dbCacheSize.length < 3 ? 0 : dbCacheSize[dbCacheSize.length - 1],
render: ({ dbCacheSize, chartstamps }) => { render: ({ dbCacheSize, chartstamps }) => {
if (dbCacheSize.length < 3) { if (dbCacheSize.length < 3) {
return '-'; return '-';
} }
return ( return (
<Sparkline width={44} height={16} stroke={1} format={formatBytes} values={dbCacheSize} stamps={chartstamps} minScale={MEMORY_SCALE} /> <Sparkline
width={44}
height={16}
stroke={1}
format={formatBytes}
values={dbCacheSize}
stamps={chartstamps}
minScale={MEMORY_SCALE}
/>
); );
} },
}; };
export const DISK_READ: Column = { export const DISK_READ: Column = {
@@ -257,16 +323,25 @@ export namespace Column {
icon: readIcon, icon: readIcon,
width: 40, width: 40,
setting: 'diskRead', setting: 'diskRead',
sortBy: ({ diskRead }) => diskRead.length < 3 ? 0 : diskRead[diskRead.length - 1], sortBy: ({ diskRead }) =>
diskRead.length < 3 ? 0 : diskRead[diskRead.length - 1],
render: ({ diskRead, chartstamps }) => { render: ({ diskRead, chartstamps }) => {
if (diskRead.length < 3) { if (diskRead.length < 3) {
return '-'; return '-';
} }
return ( return (
<Sparkline width={44} height={16} stroke={1} format={formatBandwidth} values={diskRead} stamps={chartstamps} minScale={MEMORY_SCALE} /> <Sparkline
width={44}
height={16}
stroke={1}
format={formatBandwidth}
values={diskRead}
stamps={chartstamps}
minScale={MEMORY_SCALE}
/>
); );
} },
}; };
export const DISK_WRITE: Column = { export const DISK_WRITE: Column = {
@@ -274,16 +349,25 @@ export namespace Column {
icon: writeIcon, icon: writeIcon,
width: 40, width: 40,
setting: 'diskWrite', setting: 'diskWrite',
sortBy: ({ diskWrite }) => diskWrite.length < 3 ? 0 : diskWrite[diskWrite.length - 1], sortBy: ({ diskWrite }) =>
diskWrite.length < 3 ? 0 : diskWrite[diskWrite.length - 1],
render: ({ diskWrite, chartstamps }) => { render: ({ diskWrite, chartstamps }) => {
if (diskWrite.length < 3) { if (diskWrite.length < 3) {
return '-'; return '-';
} }
return ( return (
<Sparkline width={44} height={16} stroke={1} format={formatBandwidth} values={diskWrite} stamps={chartstamps} minScale={MEMORY_SCALE} /> <Sparkline
width={44}
height={16}
stroke={1}
format={formatBandwidth}
values={diskWrite}
stamps={chartstamps}
minScale={MEMORY_SCALE}
/>
); );
} },
}; };
export const BLOCK_NUMBER: Column = { export const BLOCK_NUMBER: Column = {
@@ -292,7 +376,7 @@ export namespace Column {
width: 88, width: 88,
setting: 'blocknumber', setting: 'blocknumber',
sortBy: ({ height }) => height || 0, sortBy: ({ height }) => height || 0,
render: ({ height }) => `#${formatNumber(height)}` render: ({ height }) => `#${formatNumber(height)}`,
}; };
export const BLOCK_HASH: Column = { export const BLOCK_HASH: Column = {
@@ -301,7 +385,7 @@ export namespace Column {
width: 154, width: 154,
setting: 'blockhash', setting: 'blockhash',
sortBy: ({ hash }) => hash || '', sortBy: ({ hash }) => hash || '',
render: ({ hash }) => <Truncate position="right" text={hash} copy={true} /> render: ({ hash }) => <Truncate position="right" text={hash} copy={true} />,
}; };
export const FINALIZED: Column = { export const FINALIZED: Column = {
@@ -310,7 +394,7 @@ export namespace Column {
width: 88, width: 88,
setting: 'finalized', setting: 'finalized',
sortBy: ({ finalized }) => finalized || 0, sortBy: ({ finalized }) => finalized || 0,
render: ({ finalized }) => `#${formatNumber(finalized)}` render: ({ finalized }) => `#${formatNumber(finalized)}`,
}; };
export const FINALIZED_HASH: Column = { export const FINALIZED_HASH: Column = {
@@ -319,7 +403,9 @@ export namespace Column {
width: 154, width: 154,
setting: 'finalizedhash', setting: 'finalizedhash',
sortBy: ({ finalizedHash }) => finalizedHash || '', sortBy: ({ finalizedHash }) => finalizedHash || '',
render: ({ finalizedHash }) => <Truncate position="right" text={finalizedHash} copy={true} /> render: ({ finalizedHash }) => (
<Truncate position="right" text={finalizedHash} copy={true} />
),
}; };
export const BLOCK_TIME: Column = { export const BLOCK_TIME: Column = {
@@ -327,8 +413,8 @@ export namespace Column {
icon: blockTimeIcon, icon: blockTimeIcon,
width: 80, width: 80,
setting: 'blocktime', setting: 'blocktime',
sortBy: ({ blockTime }) => blockTime == null ? Infinity : blockTime, sortBy: ({ blockTime }) => (blockTime == null ? Infinity : blockTime),
render: ({ blockTime }) => `${secondsWithPrecision(blockTime/1000)}` render: ({ blockTime }) => `${secondsWithPrecision(blockTime / 1000)}`,
}; };
export const BLOCK_PROPAGATION: Column = { export const BLOCK_PROPAGATION: Column = {
@@ -336,8 +422,10 @@ export namespace Column {
icon: propagationTimeIcon, icon: propagationTimeIcon,
width: 58, width: 58,
setting: 'blockpropagation', setting: 'blockpropagation',
sortBy: ({ propagationTime }) => propagationTime == null ? Infinity : propagationTime, sortBy: ({ propagationTime }) =>
render: ({ propagationTime }) => propagationTime == null ? '∞' : milliOrSecond(propagationTime) propagationTime == null ? Infinity : propagationTime,
render: ({ propagationTime }) =>
propagationTime == null ? '∞' : milliOrSecond(propagationTime),
}; };
export const BLOCK_LAST_TIME: Column = { export const BLOCK_LAST_TIME: Column = {
@@ -346,7 +434,7 @@ export namespace Column {
width: 100, width: 100,
setting: 'blocklasttime', setting: 'blocklasttime',
sortBy: ({ blockTimestamp }) => blockTimestamp || 0, sortBy: ({ blockTimestamp }) => blockTimestamp || 0,
render: ({ blockTimestamp }) => <Ago when={blockTimestamp} /> render: ({ blockTimestamp }) => <Ago when={blockTimestamp} />,
}; };
export const UPTIME: Column = { export const UPTIME: Column = {
@@ -355,7 +443,7 @@ export namespace Column {
width: 58, width: 58,
setting: 'uptime', setting: 'uptime',
sortBy: ({ connectedAt }) => connectedAt || 0, sortBy: ({ connectedAt }) => connectedAt || 0,
render: ({ connectedAt }) => <Ago when={connectedAt} justTime={true} /> render: ({ connectedAt }) => <Ago when={connectedAt} justTime={true} />,
}; };
export const NETWORK_STATE: Column = { export const NETWORK_STATE: Column = {
@@ -371,33 +459,40 @@ export namespace Column {
} }
const uri = `${URI_BASE}${encodeURIComponent(chainLabel)}/${id}/`; const uri = `${URI_BASE}${encodeURIComponent(chainLabel)}/${id}/`;
return <a href={uri} target="_blank"><Icon src={externalLinkIcon} /></a>; return (
<a href={uri} target="_blank">
<Icon src={externalLinkIcon} />
</a>
);
}, },
}; };
}; }
const SEMVER_PATTERN = /^\d+\.\d+\.\d+/; const SEMVER_PATTERN = /^\d+\.\d+\.\d+/;
const BANDWIDTH_SCALE = 1024 * 1024; const BANDWIDTH_SCALE = 1024 * 1024;
const MEMORY_SCALE = 2 * 1024 * 1024; const MEMORY_SCALE = 2 * 1024 * 1024;
const URI_BASE = window.location.protocol === 'https:' const URI_BASE =
? `/network_state/` window.location.protocol === 'https:'
: `http://${window.location.hostname}:8000/network_state/`; ? `/network_state/`
: `http://${window.location.hostname}:8000/network_state/`;
function formatStamp(stamp: Types.Timestamp): string { function formatStamp(stamp: Types.Timestamp): string {
const passed = (timestamp() - stamp) / 1000 | 0; const passed = ((timestamp() - stamp) / 1000) | 0;
const hours = passed / 3600 | 0; const hours = (passed / 3600) | 0;
const minutes = (passed % 3600) / 60 | 0; const minutes = ((passed % 3600) / 60) | 0;
const seconds = (passed % 60) | 0; const seconds = passed % 60 | 0;
return hours ? `${hours}h ago` return hours
: minutes ? `${minutes}m ago` ? `${hours}h ago`
: `${seconds}s ago`; : minutes
? `${minutes}m ago`
: `${seconds}s ago`;
} }
function formatMemory(kbs: number, stamp: Maybe<Types.Timestamp>): string { function formatMemory(kbs: number, stamp: Maybe<Types.Timestamp>): string {
const ago = stamp ? ` (${formatStamp(stamp)})` : ''; const ago = stamp ? ` (${formatStamp(stamp)})` : '';
const mbs = kbs / 1024 | 0; const mbs = (kbs / 1024) | 0;
if (mbs >= 1000) { if (mbs >= 1000) {
return `${(mbs / 1024).toFixed(1)} GB${ago}`; return `${(mbs / 1024).toFixed(1)} GB${ago}`;
@@ -434,10 +529,7 @@ function formatBandwidth(bps: number, stamp: Maybe<Types.Timestamp>): string {
function formatCPU(cpu: number, stamp: Maybe<Types.Timestamp>): string { function formatCPU(cpu: number, stamp: Maybe<Types.Timestamp>): string {
const ago = stamp ? ` (${formatStamp(stamp)})` : ''; const ago = stamp ? ` (${formatStamp(stamp)})` : '';
const fractionDigits = cpu > 100 ? 0 const fractionDigits = cpu > 100 ? 0 : cpu > 10 ? 1 : cpu > 1 ? 2 : 3;
: cpu > 10 ? 1
: cpu > 1 ? 2
: 3;
return `${cpu.toFixed(fractionDigits)}%${ago}`; return `${cpu.toFixed(fractionDigits)}%${ago}`;
} }
@@ -20,19 +20,29 @@ export class HeaderCell extends React.Component<HeaderCell.Props, {}> {
public render() { public render() {
const { column, index, last } = this.props; const { column, index, last } = this.props;
const { icon, width, label } = column; const { icon, width, label } = column;
const position = index === 0 ? 'left' const position = index === 0 ? 'left' : index === last ? 'right' : 'center';
: index === last ? 'right'
: 'center';
const sortBy = this.props.sortBy.get(); const sortBy = this.props.sortBy.get();
const className = column.sortBy == null ? '' : sortBy === index || sortBy === ~index ? 'HeaderCell-sorted' : 'HeaderCell-sortable'; const className =
const i = sortBy === index ? sortAscIcon : sortBy === ~index ? sortDescIcon : icon; column.sortBy == null
? ''
: sortBy === index || sortBy === ~index
? 'HeaderCell-sorted'
: 'HeaderCell-sortable';
const i =
sortBy === index ? sortAscIcon : sortBy === ~index ? sortDescIcon : icon;
return ( return (
<th className={className} style={width ? { width } : undefined} onClick={this.toggleSort}> <th
<Tooltip text={label} inline={true} position={position}><Icon src={i} /></Tooltip> className={className}
style={width ? { width } : undefined}
onClick={this.toggleSort}
>
<Tooltip text={label} inline={true} position={position}>
<Icon src={i} />
</Tooltip>
</th> </th>
) );
} }
private toggleSort = () => { private toggleSort = () => {
@@ -50,5 +60,5 @@ export class HeaderCell extends React.Component<HeaderCell.Props, {}> {
} else { } else {
sortBy.set(index); sortBy.set(index);
} }
} };
} }
+27 -14
View File
@@ -4,7 +4,7 @@ import { Filter } from '../';
import { State as AppState, Node } from '../../state'; import { State as AppState, Node } from '../../state';
import { Row } from './'; import { Row } from './';
import { Persistent, PersistentSet } from '../../persist'; import { Persistent, PersistentSet } from '../../persist';
import { viewport } from '../../utils' import { viewport } from '../../utils';
const HEADER = 148; const HEADER = 148;
const TH_HEIGHT = 35; const TH_HEIGHT = 35;
@@ -64,7 +64,11 @@ export class List extends React.Component<List.Props, {}> {
if (nodes.length === 0) { if (nodes.length === 0) {
return ( return (
<React.Fragment> <React.Fragment>
<div className="List List-no-nodes">¯\_()_/¯<br />Nothing matches</div> <div className="List List-no-nodes">
¯\_()_/¯
<br />
Nothing matches
</div>
<Filter onChange={this.onFilterChange} /> <Filter onChange={this.onFilterChange} />
</React.Fragment> </React.Fragment>
); );
@@ -73,7 +77,7 @@ export class List extends React.Component<List.Props, {}> {
const { listStart, listEnd } = this.state; const { listStart, listEnd } = this.state;
const height = (TH_HEIGHT + nodes.length * TR_HEIGHT); const height = TH_HEIGHT + nodes.length * TR_HEIGHT;
const transform = `translateY(${listStart * TR_HEIGHT}px)`; const transform = `translateY(${listStart * TR_HEIGHT}px)`;
nodes = nodes.slice(listStart, listEnd); nodes = nodes.slice(listStart, listEnd);
@@ -84,9 +88,14 @@ export class List extends React.Component<List.Props, {}> {
<table> <table>
<Row.Header columns={selectedColumns} sortBy={sortBy} /> <Row.Header columns={selectedColumns} sortBy={sortBy} />
<tbody style={{ transform }}> <tbody style={{ transform }}>
{ {nodes.map((node) => (
nodes.map((node) => <Row key={node.id} node={node} pins={pins} columns={selectedColumns} />) <Row
} key={node.id}
node={node}
pins={pins}
columns={selectedColumns}
/>
))}
</tbody> </tbody>
</table> </table>
</div> </div>
@@ -100,7 +109,10 @@ export class List extends React.Component<List.Props, {}> {
return; return;
} }
const relativeTop = divisibleBy(window.scrollY - (HEADER + TR_HEIGHT), TR_HEIGHT * ROW_MARGIN); const relativeTop = divisibleBy(
window.scrollY - (HEADER + TR_HEIGHT),
TR_HEIGHT * ROW_MARGIN
);
if (this.relativeTop === relativeTop) { if (this.relativeTop === relativeTop) {
return; return;
@@ -110,14 +122,15 @@ export class List extends React.Component<List.Props, {}> {
this.scrolling = true; this.scrolling = true;
window.requestAnimationFrame(this.onScrollRAF); window.requestAnimationFrame(this.onScrollRAF);
} };
private onScrollRAF = () => { private onScrollRAF = () => {
const { relativeTop } = this; const { relativeTop } = this;
const { viewportHeight } = this.state; const { viewportHeight } = this.state;
const top = Math.max(relativeTop, 0); const top = Math.max(relativeTop, 0);
const height = relativeTop < 0 ? viewportHeight + relativeTop : viewportHeight; const height =
const listStart = Math.max((top / TR_HEIGHT | 0) - ROW_MARGIN, 0); relativeTop < 0 ? viewportHeight + relativeTop : viewportHeight;
const listStart = Math.max(((top / TR_HEIGHT) | 0) - ROW_MARGIN, 0);
const listEnd = listStart + ROW_MARGIN * 2 + Math.ceil(height / TR_HEIGHT); const listEnd = listStart + ROW_MARGIN * 2 + Math.ceil(height / TR_HEIGHT);
if (listStart !== this.state.listStart || listEnd !== this.state.listEnd) { if (listStart !== this.state.listStart || listEnd !== this.state.listEnd) {
@@ -125,19 +138,19 @@ export class List extends React.Component<List.Props, {}> {
} }
this.scrolling = false; this.scrolling = false;
} };
private onResize = () => { private onResize = () => {
const viewportHeight = viewport().height; const viewportHeight = viewport().height;
this.setState({ viewportHeight }); this.setState({ viewportHeight });
} };
private onFilterChange = (filter: Maybe<(node: Node) => boolean>) => { private onFilterChange = (filter: Maybe<(node: Node) => boolean>) => {
this.setState({ filter }); this.setState({ filter });
} };
} }
function divisibleBy(n: number, dividor: number): number { function divisibleBy(n: number, dividor: number): number {
return n - n % dividor; return n - (n % dividor);
} }
@@ -1,5 +1,5 @@
.Row { .Row {
color: #B5AEAE; color: #b5aeae;
cursor: pointer; cursor: pointer;
} }
@@ -12,7 +12,8 @@
text-decoration: underline; text-decoration: underline;
} }
.Row-Header th, .Row td { .Row-Header th,
.Row td {
text-align: left; text-align: left;
padding: 6px 13px; padding: 6px 13px;
height: 19px; height: 19px;
@@ -32,7 +33,7 @@
.Row-Header th.HeaderCell-sorted { .Row-Header th.HeaderCell-sorted {
cursor: pointer; cursor: pointer;
background: #E6007A; background: #e6007a;
color: #fff; color: #fff;
} }
@@ -57,17 +58,17 @@
} }
.Row-pinned td:first-child { .Row-pinned td:first-child {
border-left: 3px solid #E6007A; border-left: 3px solid #e6007a;
padding-left: 10px; padding-left: 10px;
} }
.Row-pinned td:last-child { .Row-pinned td:last-child {
border-right: 3px solid #E6007A; border-right: 3px solid #e6007a;
padding-right: 10px; padding-right: 10px;
} }
.Row-pinned.Row-synced { .Row-pinned.Row-synced {
color: #E6007A; color: #e6007a;
} }
.Row-stale { .Row-stale {
@@ -75,7 +76,7 @@
} }
.Row:hover { .Row:hover {
background-color: #1E1E1E; background-color: #1e1e1e;
} }
.Row-validator { .Row-validator {
+25 -15
View File
@@ -58,15 +58,19 @@ export class Row extends React.Component<Row.Props, Row.State> {
return ( return (
<thead> <thead>
<tr className="Row-Header"> <tr className="Row-Header">
{ {columns.map((col, index) => (
columns.map((col, index) => ( <HeaderCell
<HeaderCell key={index} column={col} index={index} last={last} sortBy={sortBy} /> key={index}
)) column={col}
} index={index}
last={last}
sortBy={sortBy}
/>
))}
</tr> </tr>
</thead> </thead>
) );
} };
public state = { update: 0 }; public state = { update: 0 };
@@ -82,8 +86,14 @@ export class Row extends React.Component<Row.Props, Row.State> {
node.unsubscribe(this.onUpdate); node.unsubscribe(this.onUpdate);
} }
public shouldComponentUpdate(nextProps: Row.Props, nextState: Row.State): boolean { public shouldComponentUpdate(
return this.props.node.id !== nextProps.node.id || this.state.update !== nextState.update; nextProps: Row.Props,
nextState: Row.State
): boolean {
return (
this.props.node.id !== nextProps.node.id ||
this.state.update !== nextState.update
);
} }
public render() { public render() {
@@ -105,9 +115,9 @@ export class Row extends React.Component<Row.Props, Row.State> {
return ( return (
<tr className={className} onClick={this.toggle}> <tr className={className} onClick={this.toggle}>
{ {columns.map(({ render }, index) => (
columns.map(({ render }, index) => <td key={index}>{render(node)}</td>) <td key={index}>{render(node)}</td>
} ))}
</tr> </tr>
); );
} }
@@ -116,13 +126,13 @@ export class Row extends React.Component<Row.Props, Row.State> {
const { pins, node } = this.props; const { pins, node } = this.props;
if (node.pinned) { if (node.pinned) {
pins.delete(node.name) pins.delete(node.name);
} else { } else {
pins.add(node.name); pins.add(node.name);
} }
} };
private onUpdate = () => { private onUpdate = () => {
this.setState({ update: this.state.update + 1 }); this.setState({ update: this.state.update + 1 });
} };
} }
@@ -18,13 +18,21 @@ export class Truncate extends React.Component<Truncate.Props, {}> {
} }
return ( return (
<Tooltip text={text} position={position} copy={copy} className="Row-Tooltip"> <Tooltip
text={text}
position={position}
copy={copy}
className="Row-Tooltip"
>
<div className="Row-truncate">{text}</div> <div className="Row-truncate">{text}</div>
</Tooltip> </Tooltip>
); );
} }
public shouldComponentUpdate(nextProps: Truncate.Props): boolean { public shouldComponentUpdate(nextProps: Truncate.Props): boolean {
return this.props.text !== nextProps.text || this.props.position !== nextProps.position; return (
this.props.text !== nextProps.text ||
this.props.position !== nextProps.position
);
} }
} }
@@ -1,4 +1,3 @@
.Location { .Location {
width: 6px; width: 6px;
height: 6px; height: 6px;
@@ -37,7 +36,7 @@
.Location-synced { .Location-synced {
z-index: 3; z-index: 3;
border-color: #E6007A; border-color: #e6007a;
} }
.Location-synced .Location-ping { .Location-synced .Location-ping {
@@ -58,7 +57,7 @@
font-family: monospace, sans-serif; font-family: monospace, sans-serif;
background: #222; background: #222;
color: #fff; color: #fff;
box-shadow: 0 3px 20px rgba(0,0,0,0.5); box-shadow: 0 3px 20px rgba(0, 0, 0, 0.5);
border-collapse: collapse; border-collapse: collapse;
} }
@@ -104,7 +103,7 @@
width: 6px; width: 6px;
height: 6px; height: 6px;
border-width: 1px; border-width: 1px;
border-color: rgba(255,255,255,1); border-color: rgba(255, 255, 255, 1);
} }
to { to {
@@ -113,7 +112,7 @@
width: 40px; width: 40px;
height: 40px; height: 40px;
border-width: 1px; border-width: 1px;
border-color: rgba(255,255,255,0); border-color: rgba(255, 255, 255, 0);
} }
} }
@@ -1,5 +1,10 @@
import * as React from 'react'; import * as React from 'react';
import { formatNumber, trimHash, milliOrSecond, secondsWithPrecision } from '../../utils'; import {
formatNumber,
trimHash,
milliOrSecond,
secondsWithPrecision,
} from '../../utils';
import { Ago, Icon, PolkadotIcon } from '../'; import { Ago, Icon, PolkadotIcon } from '../';
import { Node } from '../../state'; import { Node } from '../../state';
@@ -60,10 +65,13 @@ export class Location extends React.Component<Location.Props, Location.State> {
} }
return ( return (
<div className={className} style={{ left, top }} onMouseOver={this.onMouseOver} onMouseOut={this.onMouseOut}> <div
{ className={className}
this.state.hover ? this.renderDetails() : null style={{ left, top }}
} onMouseOver={this.onMouseOver}
onMouseOut={this.onMouseOut}
>
{this.state.hover ? this.renderDetails() : null}
<div className="Location-ping" /> <div className="Location-ping" />
</div> </div>
); );
@@ -88,10 +96,14 @@ export class Location extends React.Component<Location.Props, Location.State> {
if (validator) { if (validator) {
validatorRow = ( validatorRow = (
<tr> <tr>
<td><Icon src={nodeValidatorIcon} alt="Node" /></td> <td>
<Icon src={nodeValidatorIcon} alt="Node" />
</td>
<td colSpan={5}> <td colSpan={5}>
{trimHash(validator, 30)} {trimHash(validator, 30)}
<span className="Location-validator"><PolkadotIcon account={validator} size={16} /></span> <span className="Location-validator">
<PolkadotIcon account={validator} size={16} />
</span>
</td> </td>
</tr> </tr>
); );
@@ -101,28 +113,57 @@ export class Location extends React.Component<Location.Props, Location.State> {
<table className="Location-details Location-details"> <table className="Location-details Location-details">
<tbody> <tbody>
<tr> <tr>
<td><Icon src={nodeIcon} alt="Node" /></td><td colSpan={5}>{name}</td> <td>
<Icon src={nodeIcon} alt="Node" />
</td>
<td colSpan={5}>{name}</td>
</tr> </tr>
{validatorRow} {validatorRow}
<tr> <tr>
<td><Icon src={nodeTypeIcon} alt="Implementation" /></td><td colSpan={5}>{implementation} v{version}</td> <td>
<Icon src={nodeTypeIcon} alt="Implementation" />
</td>
<td colSpan={5}>
{implementation} v{version}
</td>
</tr> </tr>
<tr> <tr>
<td><Icon src={nodeLocationIcon} alt="Location" /></td><td colSpan={5}>{city}</td> <td>
<Icon src={nodeLocationIcon} alt="Location" />
</td>
<td colSpan={5}>{city}</td>
</tr> </tr>
<tr> <tr>
<td><Icon src={blockIcon} alt="Block" /></td><td colSpan={5}>#{formatNumber(height)}</td> <td>
<Icon src={blockIcon} alt="Block" />
</td>
<td colSpan={5}>#{formatNumber(height)}</td>
</tr> </tr>
<tr> <tr>
<td><Icon src={blockHashIcon} alt="Block Hash" /></td><td colSpan={5}>{trimHash(hash, 20)}</td> <td>
<Icon src={blockHashIcon} alt="Block Hash" />
</td>
<td colSpan={5}>{trimHash(hash, 20)}</td>
</tr> </tr>
<tr> <tr>
<td><Icon src={blockTimeIcon} alt="Block Time" /></td> <td>
<td style={{ width: 80 }}>{secondsWithPrecision(blockTime/1000)}</td> <Icon src={blockTimeIcon} alt="Block Time" />
<td><Icon src={propagationTimeIcon} alt="Block Propagation Time" /></td> </td>
<td style={{ width: 58 }}>{propagationTime == null ? '∞' : milliOrSecond(propagationTime)}</td> <td style={{ width: 80 }}>
<td><Icon src={lastTimeIcon} alt="Last Block Time" /></td> {secondsWithPrecision(blockTime / 1000)}
<td style={{ minWidth: 82 }}><Ago when={blockTimestamp} /></td> </td>
<td>
<Icon src={propagationTimeIcon} alt="Block Propagation Time" />
</td>
<td style={{ width: 58 }}>
{propagationTime == null ? '∞' : milliOrSecond(propagationTime)}
</td>
<td>
<Icon src={lastTimeIcon} alt="Last Block Time" />
</td>
<td style={{ minWidth: 82 }}>
<Ago when={blockTimestamp} />
</td>
</tr> </tr>
</tbody> </tbody>
</table> </table>
@@ -131,9 +172,9 @@ export class Location extends React.Component<Location.Props, Location.State> {
private onMouseOver = () => { private onMouseOver = () => {
this.setState({ hover: true }); this.setState({ hover: true });
} };
private onMouseOut = () => { private onMouseOut = () => {
this.setState({ hover: false }); this.setState({ hover: false });
} };
} }
+19 -11
View File
@@ -31,8 +31,8 @@ export class Map extends React.Component<Map.Props, Map.State> {
width: 0, width: 0,
height: 0, height: 0,
top: 0, top: 0,
left: 0 left: 0,
} };
public componentWillMount() { public componentWillMount() {
this.onResize(); this.onResize();
@@ -52,8 +52,7 @@ export class Map extends React.Component<Map.Props, Map.State> {
return ( return (
<React.Fragment> <React.Fragment>
<div className="Map"> <div className="Map">
{ {nodes.map((node) => {
nodes.map((node) => {
const { lat, lon } = node; const { lat, lon } = node;
const focused = filter == null || filter(node); const focused = filter == null || filter(node);
@@ -66,23 +65,32 @@ export class Map extends React.Component<Map.Props, Map.State> {
const position = this.pixelPosition(lat, lon); const position = this.pixelPosition(lat, lon);
return ( return (
<Location key={node.id} position={position} focused={focused} node={node} /> <Location
key={node.id}
position={position}
focused={focused}
node={node}
/>
); );
}) })}
}
</div> </div>
<Filter onChange={this.onFilterChange} /> <Filter onChange={this.onFilterChange} />
</React.Fragment> </React.Fragment>
); );
} }
private pixelPosition(lat: Types.Latitude, lon: Types.Longitude): Location.Position { private pixelPosition(
lat: Types.Latitude,
lon: Types.Longitude
): Location.Position {
const { state } = this; const { state } = this;
// Longitude ranges -180 (west) to +180 (east) // Longitude ranges -180 (west) to +180 (east)
// Latitude ranges +90 (north) to -90 (south) // Latitude ranges +90 (north) to -90 (south)
const left = Math.round(((180 + lon) / 360) * state.width + state.left); const left = Math.round(((180 + lon) / 360) * state.width + state.left);
const top = Math.round(((90 - lat) / 180) * state.height * MAP_HEIGHT_ADJUST + state.top); const top = Math.round(
((90 - lat) / 180) * state.height * MAP_HEIGHT_ADJUST + state.top
);
let quarter: Location.Quarter = 0; let quarter: Location.Quarter = 0;
@@ -121,9 +129,9 @@ export class Map extends React.Component<Map.Props, Map.State> {
} }
this.setState({ top, left, width, height }); this.setState({ top, left, width, height });
} };
private onFilterChange = (filter: Maybe<(node: Node) => boolean>) => { private onFilterChange = (filter: Maybe<(node: Node) => boolean>) => {
this.setState({ filter }); this.setState({ filter });
} };
} }
@@ -9,7 +9,7 @@
font-size: 30px; font-size: 30px;
padding: 16px; padding: 16px;
border-radius: 50px; border-radius: 50px;
box-shadow: rgba(0,0,0,0.5) 0 3px 20px; box-shadow: rgba(0, 0, 0, 0.5) 0 3px 20px;
} }
.OfflineIndicator-upgrade { .OfflineIndicator-upgrade {
@@ -7,16 +7,22 @@ import upgradeIcon from '../icons/flame.svg';
export namespace OfflineIndicator { export namespace OfflineIndicator {
export interface Props { export interface Props {
status: State["status"]; status: State['status'];
} }
} }
export function OfflineIndicator(props: OfflineIndicator.Props): React.ReactElement<any> | null { export function OfflineIndicator(
props: OfflineIndicator.Props
): React.ReactElement<any> | null {
switch (props.status) { switch (props.status) {
case 'online': case 'online':
return null; return null;
case 'offline': case 'offline':
return <div className="OfflineIndicator"><Icon src={offlineIcon} alt="Offline" /></div>; return (
<div className="OfflineIndicator">
<Icon src={offlineIcon} alt="Offline" />
</div>
);
case 'upgrade-requested': case 'upgrade-requested':
return ( return (
<div className="OfflineIndicator OfflineIndicator-upgrade"> <div className="OfflineIndicator OfflineIndicator-upgrade">
@@ -21,52 +21,79 @@ interface Scheme {
colors: number[]; colors: number[];
} }
const blake2 = (value: Uint8Array): Uint8Array => const blake2 = (value: Uint8Array): Uint8Array => blake2AsU8a(value, 512);
blake2AsU8a(value, 512);
const S = 64; const S = 64;
const C = S / 2; const C = S / 2;
const Z = S / 64 * 5; const Z = (S / 64) * 5;
const ZERO = blake2(new Uint8Array(32)); const ZERO = blake2(new Uint8Array(32));
const SCHEMA: Scheme[] = [ const SCHEMA: Scheme[] = [
// target // target
{ freq: 1, colors: [0, 28, 0, 0, 28, 0, 0, 28, 0, 0, 28, 0, 0, 28, 0, 0, 28, 0, 1] }, {
freq: 1,
colors: [0, 28, 0, 0, 28, 0, 0, 28, 0, 0, 28, 0, 0, 28, 0, 0, 28, 0, 1],
},
// cube // cube
{ freq: 20, colors: [0, 1, 3, 2, 4, 3, 0, 1, 3, 2, 4, 3, 0, 1, 3, 2, 4, 3, 5] }, {
freq: 20,
colors: [0, 1, 3, 2, 4, 3, 0, 1, 3, 2, 4, 3, 0, 1, 3, 2, 4, 3, 5],
},
// quazar // quazar
{ freq: 16, colors: [1, 2, 3, 1, 2, 4, 5, 5, 4, 1, 2, 3, 1, 2, 4, 5, 5, 4, 0] }, {
freq: 16,
colors: [1, 2, 3, 1, 2, 4, 5, 5, 4, 1, 2, 3, 1, 2, 4, 5, 5, 4, 0],
},
// flower // flower
{ freq: 32, colors: [0, 1, 2, 0, 1, 2, 0, 1, 2, 0, 1, 2, 0, 1, 2, 0, 1, 2, 3] }, {
freq: 32,
colors: [0, 1, 2, 0, 1, 2, 0, 1, 2, 0, 1, 2, 0, 1, 2, 0, 1, 2, 3],
},
// cyclic // cyclic
{ freq: 32, colors: [0, 1, 2, 3, 4, 5, 0, 1, 2, 3, 4, 5, 0, 1, 2, 3, 4, 5, 6] }, {
freq: 32,
colors: [0, 1, 2, 3, 4, 5, 0, 1, 2, 3, 4, 5, 0, 1, 2, 3, 4, 5, 6],
},
// vmirror // vmirror
{ freq: 128, colors: [0, 1, 2, 3, 4, 5, 3, 4, 2, 0, 1, 6, 7, 8, 9, 7, 8, 6, 10] }, {
freq: 128,
colors: [0, 1, 2, 3, 4, 5, 3, 4, 2, 0, 1, 6, 7, 8, 9, 7, 8, 6, 10],
},
// hmirror // hmirror
{ freq: 128, colors: [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 8, 6, 7, 5, 3, 4, 2, 11] }, {
freq: 128,
colors: [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 8, 6, 7, 5, 3, 4, 2, 11],
},
]; ];
const OUTER_CIRCLE: Circle = { const OUTER_CIRCLE: Circle = {
cx: C, cx: C,
cy: C, cy: C,
r: C, r: C,
fill: '#eee' fill: '#eee',
}; };
function getRotation (isSixPoint: boolean): { r: number; ro2: number; r3o4: number; ro4: number; rroot3o2: number; rroot3o4: number } { function getRotation(
const r = isSixPoint isSixPoint: boolean
? (C / 8 * 5) ): {
: (C / 4 * 3); r: number;
const rroot3o2 = r * Math.sqrt(3) / 2; ro2: number;
r3o4: number;
ro4: number;
rroot3o2: number;
rroot3o4: number;
} {
const r = isSixPoint ? (C / 8) * 5 : (C / 4) * 3;
const rroot3o2 = (r * Math.sqrt(3)) / 2;
const ro2 = r / 2; const ro2 = r / 2;
const rroot3o4 = r * Math.sqrt(3) / 4; const rroot3o4 = (r * Math.sqrt(3)) / 4;
const ro4 = r / 4; const ro4 = r / 4;
const r3o4 = r * 3 / 4; const r3o4 = (r * 3) / 4;
return { r, ro2, r3o4, ro4, rroot3o2, rroot3o4 }; return { r, ro2, r3o4, ro4, rroot3o2, rroot3o4 };
} }
function getCircleXY (isSixPoint: boolean): Array<[number, number]> { function getCircleXY(isSixPoint: boolean): Array<[number, number]> {
const { r, ro2, r3o4, ro4, rroot3o2, rroot3o4 } = getRotation(isSixPoint); const { r, ro2, r3o4, ro4, rroot3o2, rroot3o4 } = getRotation(isSixPoint);
return [ return [
@@ -88,11 +115,11 @@ function getCircleXY (isSixPoint: boolean): Array<[number, number]> {
[C + rroot3o2, C - ro2], [C + rroot3o2, C - ro2],
[C + rroot3o4, C - ro4], [C + rroot3o4, C - ro4],
[C + rroot3o4, C - r3o4], [C + rroot3o4, C - r3o4],
[C, C] [C, C],
]; ];
} }
function findScheme (d: number): Scheme { function findScheme(d: number): Scheme {
let sum = 0; let sum = 0;
const schema = SCHEMA.find((s): boolean => { const schema = SCHEMA.find((s): boolean => {
sum += s.freq; sum += s.freq;
@@ -107,19 +134,23 @@ function findScheme (d: number): Scheme {
return schema; return schema;
} }
function addressToId (address: string): Uint8Array { function addressToId(address: string): Uint8Array {
return blake2(decodeAddress(address)).map((x, i): number => (x + 256 - ZERO[i]) % 256); return blake2(decodeAddress(address)).map(
(x, i): number => (x + 256 - ZERO[i]) % 256
);
} }
function getColors (address: string): string[] { function getColors(address: string): string[] {
const total = SCHEMA.map((s): number => s.freq).reduce((a, b): number => a + b); const total = SCHEMA.map((s): number => s.freq).reduce(
(a, b): number => a + b
);
const id = addressToId(address); const id = addressToId(address);
const d = Math.floor((id[30] + id[31] * 256) % total); const d = Math.floor((id[30] + id[31] * 256) % total);
const rot = (id[28] % 6) * 3; const rot = (id[28] % 6) * 3;
const sat = (Math.floor(id[29] * 70 / 256 + 26) % 80) + 30; const sat = (Math.floor((id[29] * 70) / 256 + 26) % 80) + 30;
const scheme = findScheme(d); const scheme = findScheme(d);
const palette = Array.from(id).map((x, i): string => { const palette = Array.from(id).map((x, i): string => {
const b = (x + i % 28 * 58) % 256; const b = (x + (i % 28) * 58) % 256;
if (b === 0) { if (b === 0) {
return '#444'; return '#444';
@@ -127,27 +158,35 @@ function getColors (address: string): string[] {
return 'transparent'; return 'transparent';
} }
const h = Math.floor(b % 64 * 360 / 64); const h = Math.floor(((b % 64) * 360) / 64);
const l = [53, 15, 35, 75][Math.floor(b / 64)]; const l = [53, 15, 35, 75][Math.floor(b / 64)];
return `hsl(${h}, ${sat}%, ${l}%)`; return `hsl(${h}, ${sat}%, ${l}%)`;
}); });
return scheme.colors.map((_, i): string => return scheme.colors.map(
palette[scheme.colors[i < 18 ? (i + rot) % 18 : 18]] (_, i): string => palette[scheme.colors[i < 18 ? (i + rot) % 18 : 18]]
); );
} }
/** /**
* @description Generate a array of the circles that make up an indenticon * @description Generate a array of the circles that make up an indenticon
*/ */
export default function generate (address: string, isSixPoint = false): Circle[] { export default function generate(
address: string,
isSixPoint = false
): Circle[] {
const colors = getColors(address); const colors = getColors(address);
return [OUTER_CIRCLE].concat( return [OUTER_CIRCLE].concat(
getCircleXY(isSixPoint).map(([cx, cy], index): Circle => ({ getCircleXY(isSixPoint).map(
cx, cy, r: Z, fill: colors[index] ([cx, cy], index): Circle => ({
})) cx,
cy,
r: Z,
fill: colors[index],
})
)
); );
} }
@@ -159,29 +198,20 @@ export namespace PolkadotIcon {
} }
export class PolkadotIcon extends React.Component<PolkadotIcon.Props, {}> { export class PolkadotIcon extends React.Component<PolkadotIcon.Props, {}> {
public render (): React.ReactNode { public render(): React.ReactNode {
const { account, size } = this.props; const { account, size } = this.props;
return ( return (
<svg <svg width={size} height={size} viewBox="0 0 64 64">
width={size}
height={size}
viewBox='0 0 64 64'
>
{generate(account, false).map(this.renderCircle)} {generate(account, false).map(this.renderCircle)}
</svg> </svg>
); );
} }
private renderCircle = ({ cx, cy, r, fill }: Circle, key: number): React.ReactNode => { private renderCircle = (
return ( { cx, cy, r, fill }: Circle,
<circle key: number
key={key} ): React.ReactNode => {
cx={cx} return <circle key={key} cx={cx} cy={cy} r={r} fill={fill} />;
cy={cy} };
r={r}
fill={fill}
/>
);
}
} }
@@ -1,5 +1,5 @@
.Setting { .Setting {
color: #635F5F; color: #635f5f;
padding: 0; padding: 0;
margin: 0 0 8px 0; margin: 0 0 8px 0;
cursor: pointer; cursor: pointer;
@@ -25,8 +25,8 @@
} }
.Setting-on .Setting-switch { .Setting-on .Setting-switch {
background: #E6007A; background: #e6007a;
border-color: #E6007A; border-color: #e6007a;
} }
.Setting-knob { .Setting-knob {
@@ -19,7 +19,7 @@ export class Setting extends React.Component<Setting.Props, {}> {
const { icon, label, setting, settings } = this.props; const { icon, label, setting, settings } = this.props;
const checked = settings.get(setting); const checked = settings.get(setting);
const className = checked ? "Setting Setting-on" : "Setting"; const className = checked ? 'Setting Setting-on' : 'Setting';
return ( return (
<div className={className} onClick={this.toggle}> <div className={className} onClick={this.toggle}>
@@ -36,5 +36,5 @@ export class Setting extends React.Component<Setting.Props, {}> {
const { setting, settings } = this.props; const { setting, settings } = this.props;
settings.set(setting, !settings.get(setting)); settings.set(setting, !settings.get(setting));
} };
} }
@@ -29,16 +29,21 @@ export class Settings extends React.Component<Settings.Props, {}> {
<div className="Settings-category"> <div className="Settings-category">
<h1>List View</h1> <h1>List View</h1>
<h2>Visible Columns</h2> <h2>Visible Columns</h2>
{ {Row.columns.map(({ label, icon, setting }, index) => {
Row.columns if (!setting) {
.map(({ label, icon, setting }, index) => { return null;
if (!setting) { }
return null;
}
return <Setting key={index} setting={setting} settings={settings} icon={icon} label={label} />; return (
}) <Setting
} key={index}
setting={setting}
settings={settings}
icon={icon}
label={label}
/>
);
})}
</div> </div>
</div> </div>
); );
+25 -9
View File
@@ -1,6 +1,6 @@
import * as React from 'react'; import * as React from 'react';
import { Types, Maybe } from "@dotstats/common"; import { Types, Maybe } from '@dotstats/common';
import sparkline from "@fnando/sparkline"; import sparkline from '@fnando/sparkline';
import { Tooltip } from './'; import { Tooltip } from './';
import './Sparkline.css'; import './Sparkline.css';
@@ -33,7 +33,12 @@ export class Sparkline extends React.Component<Sparkline.Props, {}> {
public shouldComponentUpdate(nextProps: Sparkline.Props): boolean { public shouldComponentUpdate(nextProps: Sparkline.Props): boolean {
const { stroke, width, height, minScale, format } = this.props; const { stroke, width, height, minScale, format } = this.props;
if (stroke !== nextProps.stroke || width !== nextProps.width || height !== nextProps.height || format !== nextProps.format) { if (
stroke !== nextProps.stroke ||
width !== nextProps.width ||
height !== nextProps.height ||
format !== nextProps.format
) {
return true; return true;
} }
@@ -54,22 +59,33 @@ export class Sparkline extends React.Component<Sparkline.Props, {}> {
return ( return (
<Tooltip text="-" onInit={this.onTooltipInit}> <Tooltip text="-" onInit={this.onTooltipInit}>
<svg className="Sparkline" ref={this.onRef} width={width} height={height} strokeWidth={stroke} /> <svg
className="Sparkline"
ref={this.onRef}
width={width}
height={height}
strokeWidth={stroke}
/>
</Tooltip> </Tooltip>
); );
} }
private onRef = (el: SVGSVGElement) => { private onRef = (el: SVGSVGElement) => {
this.el = el; this.el = el;
} };
private onTooltipInit = (update: Tooltip.UpdateCallback) => { private onTooltipInit = (update: Tooltip.UpdateCallback) => {
this.update = update; this.update = update;
} };
private onMouseMove = (event: MouseEvent, data: { value: number, index: number }) => { private onMouseMove = (
event: MouseEvent,
data: { value: number; index: number }
) => {
const { format, stamps } = this.props; const { format, stamps } = this.props;
const str = format ? format(data.value, stamps ? stamps[data.index] : null) : `${data.value}`; const str = format
? format(data.value, stamps ? stamps[data.index] : null)
: `${data.value}`;
this.update(str); this.update(str);
} };
} }
+3 -3
View File
@@ -29,9 +29,9 @@
position: absolute; position: absolute;
left: 20px; left: 20px;
top: 20px; top: 20px;
font-size: .8em; font-size: 0.8em;
padding: 0.5em; padding: 0.5em;
border-radius: 1.25em; border-radius: 1.25em;
border: 2px solid #E6007A; border: 2px solid #e6007a;
color: #E6007A; color: #e6007a;
} }
+3 -3
View File
@@ -4,9 +4,9 @@ import { Icon } from './Icon';
export namespace Tile { export namespace Tile {
export interface Props { export interface Props {
title: string, title: string;
icon: string, icon: string;
children?: React.ReactNode, children?: React.ReactNode;
} }
} }
+1 -1
View File
@@ -12,7 +12,7 @@
left: 50%; left: 50%;
transform: translateX(-50%); transform: translateX(-50%);
display: none; display: none;
box-shadow: 0 2px 10px rgba(0,0,0,0.5); box-shadow: 0 2px 10px rgba(0, 0, 0, 0.5);
pointer-events: none; pointer-events: none;
transition: color 0.15s ease-in-out; transition: color 0.15s ease-in-out;
} }
+8 -6
View File
@@ -26,7 +26,7 @@ function copyToClipboard(text: string) {
el.select(); el.select();
document.execCommand('copy'); document.execCommand('copy');
document.body.removeChild(el); document.body.removeChild(el);
}; }
export class Tooltip extends React.Component<Tooltip.Props, Tooltip.State> { export class Tooltip extends React.Component<Tooltip.Props, Tooltip.State> {
public state = { copied: false }; public state = { copied: false };
@@ -69,7 +69,9 @@ export class Tooltip extends React.Component<Tooltip.Props, Tooltip.State> {
return ( return (
<div className={containerClass} onClick={this.onClick}> <div className={containerClass} onClick={this.onClick}>
<div className={tooltipClass} ref={this.onRef}>{copied ? 'Copied to clipboard!' : text}</div> <div className={tooltipClass} ref={this.onRef}>
{copied ? 'Copied to clipboard!' : text}
</div>
{this.props.children} {this.props.children}
</div> </div>
); );
@@ -77,11 +79,11 @@ export class Tooltip extends React.Component<Tooltip.Props, Tooltip.State> {
private onRef = (el: HTMLDivElement) => { private onRef = (el: HTMLDivElement) => {
this.el = el; this.el = el;
} };
private update = (text: string) => { private update = (text: string) => {
this.el.textContent = text; this.el.textContent = text;
} };
private onClick = (event: React.MouseEvent<HTMLDivElement>) => { private onClick = (event: React.MouseEvent<HTMLDivElement>) => {
if (this.props.copy !== true) { if (this.props.copy !== true) {
@@ -96,9 +98,9 @@ export class Tooltip extends React.Component<Tooltip.Props, Tooltip.State> {
this.setState({ copied: true }); this.setState({ copied: true });
this.timer = setTimeout(this.restore, 2000); this.timer = setTimeout(this.restore, 2000);
} };
private restore = () => { private restore = () => {
this.setState({ copied: false }); this.setState({ copied: false });
} };
} }
+1 -1
View File
@@ -1 +1 @@
declare module "*.svg"; declare module '*.svg';
+1 -4
View File
@@ -4,9 +4,6 @@ import App from './App';
import './index.css'; import './index.css';
import { unregister } from './registerServiceWorker'; import { unregister } from './registerServiceWorker';
ReactDOM.render( ReactDOM.render(<App />, document.getElementById('root') as HTMLElement);
<App />,
document.getElementById('root') as HTMLElement
);
unregister(); unregister();
+10 -3
View File
@@ -5,7 +5,11 @@ export class Persistent<Data> {
private readonly key: string; private readonly key: string;
private value: Data; private value: Data;
constructor(key: string, initial: Data, onChange: (value: Readonly<Data>) => void) { constructor(
key: string,
initial: Data,
onChange: (value: Readonly<Data>) => void
) {
this.key = key; this.key = key;
this.onChange = onChange; this.onChange = onChange;
@@ -23,7 +27,7 @@ export class Persistent<Data> {
window.addEventListener('storage', (event) => { window.addEventListener('storage', (event) => {
if (event.key === this.key) { if (event.key === this.key) {
this.value = parse(event.newValue as any as Stringified<Data>); this.value = parse((event.newValue as any) as Stringified<Data>);
this.onChange(this.value); this.onChange(this.value);
} }
@@ -36,7 +40,10 @@ export class Persistent<Data> {
public set(value: Data) { public set(value: Data) {
this.value = value; this.value = value;
window.localStorage.setItem(this.key, stringify(this.value) as any as string); window.localStorage.setItem(
this.key,
(stringify(this.value) as any) as string
);
this.onChange(this.value); this.onChange(this.value);
} }
} }
@@ -5,7 +5,9 @@ export class PersistentSet<Item> {
private value: Set<Item>; private value: Set<Item>;
constructor(key: string, onChange: (value: Set<Item>) => void) { constructor(key: string, onChange: (value: Set<Item>) => void) {
this.inner = new Persistent(key, [], (raw: Readonly<Item[]>) => onChange(this.value = new Set(raw as Item[]))); this.inner = new Persistent(key, [], (raw: Readonly<Item[]>) =>
onChange((this.value = new Set(raw as Item[])))
);
this.value = new Set(this.inner.get() as Item[]); this.value = new Set(this.inner.get() as Item[]);
} }
@@ -11,7 +11,7 @@
export function unregister() { export function unregister() {
if ('serviceWorker' in navigator) { if ('serviceWorker' in navigator) {
navigator.serviceWorker.ready.then(registration => { navigator.serviceWorker.ready.then((registration) => {
registration.unregister(); registration.unregister();
}); });
} }
+14 -6
View File
@@ -7,8 +7,10 @@ export class Node {
public static compare(a: Node, b: Node): number { public static compare(a: Node, b: Node): number {
if (a.pinned === b.pinned && a.stale === b.stale) { if (a.pinned === b.pinned && a.stale === b.stale) {
if (a.height === b.height) { if (a.height === b.height) {
const aPropagation = a.propagationTime == null ? Infinity : a.propagationTime as number; const aPropagation =
const bPropagation = b.propagationTime == null ? Infinity : b.propagationTime as number; a.propagationTime == null ? Infinity : (a.propagationTime as number);
const bPropagation =
b.propagationTime == null ? Infinity : (b.propagationTime as number);
// Ascending sort by propagation time // Ascending sort by propagation time
return aPropagation - bPropagation; return aPropagation - bPropagation;
@@ -74,7 +76,7 @@ export class Node {
nodeHardware: Types.NodeHardware, nodeHardware: Types.NodeHardware,
blockDetails: Types.BlockDetails, blockDetails: Types.BlockDetails,
location: Maybe<Types.NodeLocation>, location: Maybe<Types.NodeLocation>,
connectedAt: Types.Timestamp, connectedAt: Types.Timestamp
) { ) {
const [name, implementation, version, validator, networkId] = nodeDetails; const [name, implementation, version, validator, networkId] = nodeDetails;
@@ -88,7 +90,9 @@ export class Node {
this.networkId = networkId; this.networkId = networkId;
this.connectedAt = connectedAt; this.connectedAt = connectedAt;
const [major = 0, minor = 0, patch = 0] = (version || '0.0.0').split('.').map((n) => parseInt(n, 10) | 0); const [major = 0, minor = 0, patch = 0] = (version || '0.0.0')
.split('.')
.map((n) => parseInt(n, 10) | 0);
this.sortableName = name.toLocaleLowerCase(); this.sortableName = name.toLocaleLowerCase();
this.sortableVersion = (major * 1000 + minor * 100 + patch) | 0; this.sortableVersion = (major * 1000 + minor * 100 + patch) | 0;
@@ -262,8 +266,12 @@ export interface State {
selectedColumns: Column[]; selectedColumns: Column[];
} }
export type Update = <K extends keyof State>(changes: Pick<State, K> | null) => Readonly<State>; export type Update = <K extends keyof State>(
export type UpdateBound = <K extends keyof State>(changes: Pick<State, K> | null) => void; changes: Pick<State, K> | null
) => Readonly<State>;
export type UpdateBound = <K extends keyof State>(
changes: Pick<State, K> | null
) => void;
export interface ChainData { export interface ChainData {
label: Types.ChainLabel; label: Types.ChainLabel;
+22 -10
View File
@@ -6,14 +6,20 @@ export interface Viewport {
} }
export function viewport(): Viewport { export function viewport(): Viewport {
const width = Math.max(document.documentElement.clientWidth, window.innerWidth || 0); const width = Math.max(
const height = Math.max(document.documentElement.clientHeight, window.innerHeight || 0); document.documentElement.clientWidth,
window.innerWidth || 0
);
const height = Math.max(
document.documentElement.clientHeight,
window.innerHeight || 0
);
return { width, height }; return { width, height };
} }
export function formatNumber(num: number): string { export function formatNumber(num: number): string {
const input = num.toString(); const input = num.toString();
let output = ''; let output = '';
let length = input.length; let length = input.length;
@@ -38,7 +44,9 @@ export function trimHash(hash: string, length: number): string {
return hash.substr(0, side) + '..' + hash.substr(-side, side); return hash.substr(0, side) + '..' + hash.substr(-side, side);
} }
export function milliOrSecond(num: Types.Milliseconds | Types.PropagationTime): string { export function milliOrSecond(
num: Types.Milliseconds | Types.PropagationTime
): string {
if (num < 10000) { if (num < 10000) {
return `${num}ms`; return `${num}ms`;
} }
@@ -47,21 +55,25 @@ export function milliOrSecond(num: Types.Milliseconds | Types.PropagationTime):
} }
export function secondsWithPrecision(num: number): string { export function secondsWithPrecision(num: number): string {
const intString = (num | 0).toString() const intString = (num | 0).toString();
const intDigits = intString.length; const intDigits = intString.length;
switch (intDigits) { switch (intDigits) {
case 1: return num.toFixed(3) + 's'; case 1:
case 2: return num.toFixed(2) + 's'; return num.toFixed(3) + 's';
case 3: return num.toFixed(1) + 's'; case 2:
default: return intString + 's'; return num.toFixed(2) + 's';
case 3:
return num.toFixed(1) + 's';
default:
return intString + 's';
} }
} }
export interface HashData { export interface HashData {
tab?: string; tab?: string;
chain?: Types.ChainLabel; chain?: Types.ChainLabel;
}; }
export function getHashData(): HashData { export function getHashData(): HashData {
const { hash } = window.location; const { hash } = window.location;
+10 -6
View File
@@ -1,19 +1,23 @@
{ {
"extends": ["tslint:recommended", "tslint-react", "tslint-config-prettier"], "extends": [
"tslint:recommended",
"tslint-react",
"tslint-plugin-prettier",
"tslint-config-prettier"
],
"linterOptions": { "linterOptions": {
"exclude": [ "exclude": ["config/**/*.js", "node_modules/**/*.ts"]
"config/**/*.js",
"node_modules/**/*.ts"
]
}, },
"rules": { "rules": {
"prettier": true,
"ordered-imports": false, "ordered-imports": false,
"object-literal-sort-keys": false, "object-literal-sort-keys": false,
"no-console": false, "no-console": false,
"no-unused-variable": [true, {"ignore-pattern": "^_"}],
"no-empty": false, "no-empty": false,
"no-namespace": false, "no-namespace": false,
"no-bitwise": false, "no-bitwise": false,
"quotemark": [true, "single", "jsx-double"],
"semicolon": [true, "always", "ignore-bound-class-methods"],
"interface-name": false "interface-name": false
} }
} }
+688 -6
View File
File diff suppressed because it is too large Load Diff