mirror of
https://github.com/pezkuwichain/pezkuwi-telemetry.git
synced 2026-06-12 13:31:15 +00:00
Grandpa visualization optimizations + improvements (#150)
* Prefix CSS rules properly * Fix Jdenticon placeholder * Implement shouldComponentUpdate() * Force casting of block numbers in backend * Ensure array is properly sorted * Fix backfilling and hold only limited blocks in memory * Use proper ellipsis * Display note if no grandpa data available yet * Apply flexing only when two blocks are displayed * Display consensus icons above all other elements * Type authoritySetId properly * Display loading screen only when necessary * Only measure when necessary * Reset state on tab change * Remove tooltips and add keys * fix: Remove some `any` types, fix list view CSS * Fix updateState type * Add keys to more elements * Limit number of authorities for which vis works
This commit is contained in:
committed by
Maciej Hirsz
parent
26000f3e8a
commit
7add77137a
@@ -321,7 +321,8 @@ export default class Node {
|
||||
target_hash: targetHash,
|
||||
} = message;
|
||||
const voter = this.extractVoter(message.voter);
|
||||
this.events.emit('afg-received-precommit', targetNumber, targetHash, voter);
|
||||
const number = parseInt(String(targetNumber), 10) as Types.BlockNumber;
|
||||
this.events.emit('afg-received-precommit', number, targetHash, voter);
|
||||
}
|
||||
|
||||
private onAfgReceivedPrevote(message: AfgReceivedPrevote) {
|
||||
@@ -330,7 +331,8 @@ export default class Node {
|
||||
target_hash: targetHash,
|
||||
} = message;
|
||||
const voter = this.extractVoter(message.voter);
|
||||
this.events.emit('afg-received-prevote', targetNumber, targetHash, voter);
|
||||
const number = parseInt(String(targetNumber), 10) as Types.BlockNumber;
|
||||
this.events.emit('afg-received-prevote', number, targetHash, voter);
|
||||
}
|
||||
|
||||
private onAfgAuthoritySet(message: AfgAuthoritySet) {
|
||||
@@ -346,7 +348,8 @@ export default class Node {
|
||||
|
||||
if (JSON.stringify(this.authorities) !== String(message.authorities) ||
|
||||
this.authoritySetId !== authoritySetId) {
|
||||
this.events.emit('authority-set-changed', authorities, authoritySetId, number, hash);
|
||||
const no = parseInt(String(number), 10) as Types.BlockNumber;
|
||||
this.events.emit('authority-set-changed', authorities, authoritySetId, no, hash);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -355,7 +358,8 @@ export default class Node {
|
||||
finalized_number: finalizedNumber,
|
||||
finalized_hash: finalizedHash,
|
||||
} = message;
|
||||
this.events.emit('afg-finalized', finalizedNumber, finalizedHash);
|
||||
const number = parseInt(String(finalizedNumber), 10) as Types.BlockNumber;
|
||||
this.events.emit('afg-finalized', number, finalizedHash);
|
||||
}
|
||||
|
||||
private extractVoter(message_voter: String): Types.Address {
|
||||
|
||||
@@ -9,4 +9,4 @@ import * as FeedMessage from './feed';
|
||||
export { Types, FeedMessage };
|
||||
|
||||
// Increment this if breaking changes were made to types in `feed.ts`
|
||||
export const VERSION: Types.FeedVersion = 24 as Types.FeedVersion;
|
||||
export const VERSION: Types.FeedVersion = 22 as Types.FeedVersion;
|
||||
|
||||
@@ -40,7 +40,8 @@ export declare type Authority = {
|
||||
export declare type Authorities = Array<Address>;
|
||||
export declare type AuthoritySetId = Opaque<number, 'AuthoritySetId'>;
|
||||
export declare type AuthoritySetInfo = [AuthoritySetId, Authorities, Address, BlockNumber, BlockHash];
|
||||
export declare type ConsensusInfo = Array<[BlockNumber, ConsensusView]>;
|
||||
export declare type ConsensusItem = [BlockNumber, ConsensusView];
|
||||
export declare type ConsensusInfo = Array<ConsensusItem>;
|
||||
export declare type ConsensusView = Map<Address, ConsensusState>;
|
||||
export declare type ConsensusState = Map<Address, ConsensusDetail>;
|
||||
export declare type ConsensusDetail = {
|
||||
|
||||
@@ -1,13 +1,15 @@
|
||||
import { Types, Maybe } from '@dotstats/common';
|
||||
import { State } from './state';
|
||||
import { Types } from '@dotstats/common';
|
||||
import { State, UpdateBound } from './state';
|
||||
|
||||
const BLOCKS_LIMIT = 50;
|
||||
|
||||
export class AfgHandling {
|
||||
private updateState: (state: any) => void;
|
||||
private getState: () => State;
|
||||
private updateState: UpdateBound;
|
||||
private getState: () => Readonly<State>;
|
||||
|
||||
constructor(
|
||||
updateState: (state: any) => void,
|
||||
getState: () => State,
|
||||
updateState: UpdateBound,
|
||||
getState: () => Readonly<State>,
|
||||
) {
|
||||
this.updateState = updateState;
|
||||
this.getState = getState;
|
||||
@@ -17,9 +19,22 @@ export class AfgHandling {
|
||||
authoritySetId: Types.AuthoritySetId,
|
||||
authorities: Types.Authorities,
|
||||
) {
|
||||
if (authoritySetId !== this.getState().authoritySetId) {
|
||||
if (this.getState().authoritySetId != null && authoritySetId !== this.getState().authoritySetId) {
|
||||
// the visualization is restarted when we receive a new auhority set
|
||||
this.updateState({authoritySetId, authorities, consensusInfo: []});
|
||||
this.updateState({
|
||||
authoritySetId,
|
||||
authorities,
|
||||
consensusInfo: [],
|
||||
displayConsensusLoadingScreen: false,
|
||||
});
|
||||
} else if (this.getState().authoritySetId == null) {
|
||||
// initial display
|
||||
this.updateState({
|
||||
authoritySetId,
|
||||
authorities,
|
||||
consensusInfo: [],
|
||||
displayConsensusLoadingScreen: true,
|
||||
});
|
||||
}
|
||||
return null;
|
||||
}
|
||||
@@ -29,19 +44,58 @@ export class AfgHandling {
|
||||
finalizedNumber: Types.BlockNumber,
|
||||
finalizedHash: Types.BlockHash,
|
||||
) {
|
||||
const consensusInfo = this.getState().consensusInfo;
|
||||
this.markFinalized(addr, finalizedNumber, finalizedHash);
|
||||
const state = this.getState();
|
||||
if (finalizedNumber < state.best - BLOCKS_LIMIT) {
|
||||
return;
|
||||
};
|
||||
|
||||
const op = (i: Types.BlockNumber, view: Types.ConsensusView) => {
|
||||
const consensusDetail = view[addr][addr];
|
||||
const data = {
|
||||
Finalized: true,
|
||||
FinalizedHash: finalizedHash,
|
||||
FinalizedHeight: finalizedNumber,
|
||||
|
||||
// this is extrapolated. if this app was just started up we
|
||||
// might not yet have received prevotes/precommits. but
|
||||
// those are a necessary precondition for finalization, so
|
||||
// we can set them and display them in the ui.
|
||||
Prevote: true,
|
||||
Precommit: true,
|
||||
} as Types.ConsensusDetail;
|
||||
this.initialiseConsensusView(state.consensusInfo, finalizedNumber, addr, addr);
|
||||
|
||||
this.updateConsensusInfo(state.consensusInfo, finalizedNumber, addr, addr, data as Partial<Types.ConsensusDetail>);
|
||||
|
||||
// Finalizing a block implicitly includes finalizing all
|
||||
// preceding blocks. This function marks the preceding
|
||||
// blocks as implicitly finalized on and stores a pointer
|
||||
// to the block which contains the explicit finalization.
|
||||
const op = (i: Types.BlockNumber, index: number) : boolean => {
|
||||
const consensusDetail = state.consensusInfo[index][1][addr][addr];
|
||||
if (consensusDetail.Finalized || consensusDetail.ImplicitFinalized) {
|
||||
return false;
|
||||
}
|
||||
|
||||
this.markImplicitlyFinalized(i, addr, finalizedNumber, addr);
|
||||
state.consensusInfo[index][1][addr][addr] = {
|
||||
Finalized: true,
|
||||
FinalizedHeight: i,
|
||||
ImplicitFinalized: true,
|
||||
ImplicitPointer: finalizedNumber,
|
||||
|
||||
// this is extrapolated. if this app was just started up we
|
||||
// might not yet have received prevotes/precommits. but
|
||||
// those are a necessary precondition for finalization, so
|
||||
// we can set them and display them in the ui.
|
||||
Prevote: true,
|
||||
Precommit: true,
|
||||
ImplicitPrevote: true,
|
||||
ImplicitPrecommit: true,
|
||||
};
|
||||
return true;
|
||||
};
|
||||
this.backfill(consensusInfo, finalizedNumber, op, addr, addr);
|
||||
this.backfill(state.consensusInfo, finalizedNumber, op, addr, addr);
|
||||
|
||||
this.pruneBlocks(state.consensusInfo);
|
||||
this.updateState({consensusInfo: state.consensusInfo});
|
||||
}
|
||||
|
||||
public receivedPre(
|
||||
@@ -51,95 +105,47 @@ export class AfgHandling {
|
||||
voter: Types.Address,
|
||||
what: string,
|
||||
) {
|
||||
const data = what === "prevote" ? { Prevote: true } : { Precommit: true };
|
||||
this.updateConsensusInfo(height, addr, voter, data as Partial<Types.ConsensusDetail>);
|
||||
const state = this.getState();
|
||||
if (height < state.best - BLOCKS_LIMIT) {
|
||||
return;
|
||||
};
|
||||
|
||||
const op = (i: Types.BlockNumber, view: Types.ConsensusView) => {
|
||||
const consensusDetail = view[addr][voter];
|
||||
if (consensusDetail.Prevote || consensusDetail.ImplicitPrevote) {
|
||||
const data = what === "prevote" ? { Prevote: true } : { Precommit: true };
|
||||
this.initialiseConsensusView(state.consensusInfo, height, addr, voter);
|
||||
this.updateConsensusInfo(state.consensusInfo, height, addr, voter, data as Partial<Types.ConsensusDetail>);
|
||||
|
||||
// A Prevote or Precommit on a block implicitly includes
|
||||
// a vote on all preceding blocks. This function marks
|
||||
// the preceding blocks as implicitly voted on and stores
|
||||
// a pointer to the block which contains the explicit vote.
|
||||
const op = (i: Types.BlockNumber, index: number) : boolean => {
|
||||
const consensusDetail = state.consensusInfo[index][1][addr][voter];
|
||||
if (what === "prevote" && (consensusDetail.Prevote || consensusDetail.ImplicitPrevote)) {
|
||||
return false;
|
||||
}
|
||||
if (what === "precommit" && (consensusDetail.Precommit || consensusDetail.ImplicitPrecommit)
|
||||
|
||||
// because of extrapolation a prevote needs to be set as well.
|
||||
// if it is not we continue backfilling (and set it during that process).
|
||||
&& (consensusDetail.Prevote || consensusDetail.ImplicitPrevote)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
this.markImplicitlyPre(i, addr, height, what, voter);
|
||||
if (what === "prevote") {
|
||||
consensusInfo[index][1][addr][voter].ImplicitPrevote = true;
|
||||
} else if (what === "precommit") {
|
||||
consensusInfo[index][1][addr][voter].ImplicitPrecommit = true;
|
||||
|
||||
// Extrapolate. Precommit implies Prevote.
|
||||
consensusInfo[index][1][addr][voter].ImplicitPrevote = true;
|
||||
}
|
||||
consensusInfo[index][1][addr][voter].ImplicitPointer = height;
|
||||
return true;
|
||||
};
|
||||
this.backfill(this.getState().consensusInfo, height, op, addr, voter);
|
||||
}
|
||||
|
||||
private markFinalized(
|
||||
addr: Types.Address,
|
||||
finalizedHeight: Types.BlockNumber,
|
||||
finalizedHash: Types.BlockHash,
|
||||
) {
|
||||
const data = {
|
||||
Finalized: true,
|
||||
FinalizedHash: finalizedHash,
|
||||
FinalizedHeight: finalizedHeight,
|
||||
|
||||
// this is extrapolated. if this app was just started up we
|
||||
// might not yet have received prevotes/precommits. but
|
||||
// those are a necessary precondition for finalization, so
|
||||
// we can set them and display them in the ui.
|
||||
Prevote: true,
|
||||
Precommit: true,
|
||||
} as Types.ConsensusDetail;
|
||||
this.updateConsensusInfo(finalizedHeight, addr, addr, data);
|
||||
}
|
||||
|
||||
// A Prevote or Precommit on a block implicitly includes
|
||||
// a vote on all preceding blocks. This function marks
|
||||
// the preceding blocks as implicitly voted on and stores
|
||||
// a pointer to the block which contains the explicit vote.
|
||||
private markImplicitlyPre(
|
||||
height: Types.BlockNumber,
|
||||
addr: Types.Address,
|
||||
where: Types.BlockNumber,
|
||||
what: string,
|
||||
voter: Types.Address,
|
||||
) {
|
||||
const consensusInfo = this.getState().consensusInfo;
|
||||
const consensusView = this.initialiseConsensusView(consensusInfo, height, addr, voter);
|
||||
|
||||
if (what === "prevote") {
|
||||
consensusView[addr][voter].ImplicitPrevote = true;
|
||||
} else if (what === "precommit") {
|
||||
consensusView[addr][voter].ImplicitPrecommit = true;
|
||||
}
|
||||
consensusView[addr][voter].ImplicitPointer = where;
|
||||
|
||||
this.updateState({consensusInfo});
|
||||
}
|
||||
|
||||
// Finalizing a block implicitly includes finalizing all
|
||||
// preceding blocks. This function marks the preceding
|
||||
// blocks as implicitly finalized on and stores a pointer
|
||||
// to the block which contains the explicit finalization.
|
||||
private markImplicitlyFinalized(
|
||||
height: Types.BlockNumber,
|
||||
addr: Types.Address,
|
||||
to: Types.BlockNumber,
|
||||
voter: Types.Address,
|
||||
) {
|
||||
const consensusInfo = this.getState().consensusInfo;
|
||||
const consensusView = this.initialiseConsensusView(consensusInfo, height, addr, voter);
|
||||
|
||||
const consensusDetail = {
|
||||
Finalized: true,
|
||||
FinalizedHeight: height,
|
||||
ImplicitFinalized: true,
|
||||
ImplicitPointer: to,
|
||||
|
||||
// this is extrapolated. if this app was just started up we
|
||||
// might not yet have received prevotes/precommits. but
|
||||
// those are a necessary precondition for finalization, so
|
||||
// we can set them and display them in the ui.
|
||||
Prevote: true,
|
||||
Precommit: true,
|
||||
ImplicitPrevote: true,
|
||||
ImplicitPrecommit: true,
|
||||
};
|
||||
consensusView[addr][voter] = consensusDetail;
|
||||
this.backfill(consensusInfo, height, op, addr, voter);
|
||||
|
||||
this.pruneBlocks(consensusInfo);
|
||||
this.updateState({consensusInfo});
|
||||
}
|
||||
|
||||
@@ -149,21 +155,27 @@ export class AfgHandling {
|
||||
height: Types.BlockNumber,
|
||||
own: Types.Address,
|
||||
other: Types.Address,
|
||||
) : Types.ConsensusView {
|
||||
) {
|
||||
const found =
|
||||
consensusInfo.find(([blockNumber,]) => blockNumber === height);
|
||||
|
||||
let consensusView;
|
||||
if (found) {
|
||||
[, consensusView] = found;
|
||||
this.initialiseConsensusViewByRef(consensusView, own, other);
|
||||
} else {
|
||||
consensusView = {} as Types.ConsensusView;
|
||||
consensusInfo.unshift([height, consensusView]);
|
||||
|
||||
this.initialiseConsensusViewByRef(consensusView, own, other);
|
||||
|
||||
const item: Types.ConsensusItem = [height, consensusView];
|
||||
const insertPos = consensusInfo.findIndex(([elHeight, elView]) => elHeight < height);
|
||||
if (insertPos >= 0) {
|
||||
consensusInfo.splice(insertPos, 0, item);
|
||||
} else {
|
||||
consensusInfo.push(item);
|
||||
}
|
||||
}
|
||||
|
||||
this.initialiseConsensusViewByRef(consensusView, own, other);
|
||||
|
||||
return consensusView;
|
||||
}
|
||||
|
||||
// Initializes the `ConsensusView` with empty objects.
|
||||
@@ -188,41 +200,84 @@ export class AfgHandling {
|
||||
// Returns block number until which we backfilled.
|
||||
private backfill(
|
||||
consensusInfo: Types.ConsensusInfo,
|
||||
to: Types.BlockNumber,
|
||||
f: Maybe<(i: Types.BlockNumber, consensusView: Types.ConsensusView) => boolean>,
|
||||
start: Types.BlockNumber,
|
||||
f: (i: Types.BlockNumber, index: number) => boolean,
|
||||
own: Types.Address,
|
||||
other: Types.Address,
|
||||
): Types.BlockNumber {
|
||||
for (const [height, consensusView] of consensusInfo) {
|
||||
if (height >= to) {
|
||||
continue;
|
||||
) {
|
||||
// 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
|
||||
// sense to backfill, because we could potentially backfill
|
||||
// until 0 (which could be unfortunate if the first received
|
||||
// block is e.g. 28317.
|
||||
if (consensusInfo.length < 2) {
|
||||
return;
|
||||
}
|
||||
|
||||
let firstBlockNumber = consensusInfo[consensusInfo.length - 1][0];
|
||||
const limit = this.getState().best - BLOCKS_LIMIT;
|
||||
if (firstBlockNumber < limit) {
|
||||
firstBlockNumber = limit as Types.BlockNumber;
|
||||
}
|
||||
|
||||
if (start - 1 < firstBlockNumber) {
|
||||
// if the first block which would be backfilled is already
|
||||
// less than the first block number we can abort.
|
||||
//
|
||||
// this can happen if e.g. one authority is hanging behind,
|
||||
// most of them could e.g. be at 3000 and one is hanging behind
|
||||
// and sending info for 2000. then we can't start backfilling
|
||||
// from 2000.
|
||||
return;
|
||||
}
|
||||
|
||||
let counter = 0;
|
||||
while (start-- > 0) {
|
||||
counter++;
|
||||
if (counter >= BLOCKS_LIMIT) {
|
||||
break;
|
||||
}
|
||||
|
||||
this.initialiseConsensusViewByRef(consensusView, own, other);
|
||||
|
||||
const cont = f ? f(height, consensusView) : true;
|
||||
const startBlockNumber = start as Types.BlockNumber;
|
||||
this.initialiseConsensusView(consensusInfo, startBlockNumber, own, other);
|
||||
const index =
|
||||
consensusInfo.findIndex(([blockNumber,]) => blockNumber === start);
|
||||
const cont = f(start, index);
|
||||
if (!cont) {
|
||||
break;
|
||||
}
|
||||
|
||||
// we don't want to fill into nirvana
|
||||
const firstBlockReached = startBlockNumber <= firstBlockNumber;
|
||||
if (firstBlockReached) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
return to;
|
||||
}
|
||||
|
||||
private updateConsensusInfo(
|
||||
consensusInfo: Types.ConsensusInfo,
|
||||
height: Types.BlockNumber,
|
||||
addr: Types.Address,
|
||||
voter: Types.Address,
|
||||
data: Partial<Types.ConsensusDetail>,
|
||||
) {
|
||||
const consensusInfo = this.getState().consensusInfo;
|
||||
const consensusView = this.initialiseConsensusView(consensusInfo, height, addr, voter);
|
||||
const found =
|
||||
consensusInfo.findIndex(([blockNumber,]) => blockNumber === height);
|
||||
if (found < 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
for (const k in data) {
|
||||
if (data.hasOwnProperty(k)) {
|
||||
consensusView[addr][voter][k] = data[k];
|
||||
consensusInfo[found][1][addr][voter][k] = data[k];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
this.updateState({consensusInfo});
|
||||
private pruneBlocks(consensusInfo: Types.ConsensusInfo) {
|
||||
if (consensusInfo.length >= BLOCKS_LIMIT) {
|
||||
consensusInfo.length = BLOCKS_LIMIT;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -54,8 +54,9 @@ export default class App extends React.Component<{}, State> {
|
||||
best: 0 as Types.BlockNumber,
|
||||
finalized: 0 as Types.BlockNumber,
|
||||
consensusInfo: new Array() as Types.ConsensusInfo,
|
||||
displayConsensusLoadingScreen: true,
|
||||
authorities: new Array() as Types.Authorities,
|
||||
authoritySetId: -1 as Types.AuthoritySetId,
|
||||
authoritySetId: null,
|
||||
sendFinality: false,
|
||||
blockTimestamp: 0 as Types.Timestamp,
|
||||
blockAverage: null,
|
||||
@@ -65,6 +66,7 @@ export default class App extends React.Component<{}, State> {
|
||||
nodes: new SortedCollection(Node.compare),
|
||||
settings: this.settings.raw(),
|
||||
pins: this.pins.get(),
|
||||
tabChanged: false,
|
||||
};
|
||||
|
||||
this.connection = Connection.create(this.pins, (changes) => {
|
||||
@@ -85,7 +87,7 @@ export default class App extends React.Component<{}, State> {
|
||||
return (
|
||||
<div className="App App-no-telemetry">
|
||||
<OfflineIndicator status={status} />
|
||||
Waiting for telemetry data...
|
||||
Waiting for telemetry…
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -99,6 +101,12 @@ export default class App extends React.Component<{}, State> {
|
||||
);
|
||||
}
|
||||
|
||||
public componentDidUpdate() {
|
||||
if (this.state.tabChanged === true) {
|
||||
this.setState({tabChanged: false});
|
||||
}
|
||||
}
|
||||
|
||||
public componentWillMount() {
|
||||
window.addEventListener('keydown', this.onKeyPress);
|
||||
}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { VERSION, timestamp, FeedMessage, Types, Maybe, sleep } from '@dotstats/common';
|
||||
import { State, Update, Node } from './state';
|
||||
import { State, Update, Node, UpdateBound } from './state';
|
||||
import { PersistentSet } from './persist';
|
||||
import { getHashData, setHashData } from './utils';
|
||||
import { AfgHandling } from './AfgHandling';
|
||||
@@ -14,11 +14,13 @@ export class Connection {
|
||||
return new Connection(await Connection.socket(), update, pins);
|
||||
}
|
||||
|
||||
private static readonly address = window.location.protocol === 'https:'
|
||||
private static readonly debug: number = 2;
|
||||
|
||||
private static readonly address1 = window.location.protocol === 'https:'
|
||||
? `wss://${window.location.hostname}/feed/`
|
||||
: `ws://${window.location.hostname}:8080`;
|
||||
|
||||
// private static readonly address = 'wss://telemetry.polkadot.io/feed/';
|
||||
private static readonly address2 = 'wss://telemetry.polkadot.io/feed/';
|
||||
|
||||
private static async socket(): Promise<WebSocket> {
|
||||
let socket = await Connection.trySocket();
|
||||
@@ -52,7 +54,7 @@ export class Connection {
|
||||
resolve(null);
|
||||
}
|
||||
|
||||
const socket = new WebSocket(Connection.address);
|
||||
const socket = new WebSocket(Connection.debug === 1 ? Connection.address1 : Connection.address2);
|
||||
|
||||
socket.addEventListener('open', onSuccess);
|
||||
socket.addEventListener('error', onFailure);
|
||||
@@ -78,7 +80,15 @@ export class Connection {
|
||||
}
|
||||
|
||||
public subscribe(chain: Types.ChainLabel) {
|
||||
setHashData({ chain });
|
||||
if (this.state.subscribed != null && this.state.subscribed !== chain) {
|
||||
this.state = this.update({
|
||||
tabChanged: true,
|
||||
});
|
||||
setHashData({ chain, tab: 'list' });
|
||||
} else {
|
||||
setHashData({ chain });
|
||||
}
|
||||
|
||||
this.socket.send(`subscribe:${chain}`);
|
||||
}
|
||||
|
||||
@@ -88,8 +98,16 @@ export class Connection {
|
||||
this.socket.send(`send-finality:${chain}`);
|
||||
}
|
||||
|
||||
public resetConsensus() {
|
||||
this.state = this.update({
|
||||
consensusInfo: new Array() as Types.ConsensusInfo,
|
||||
displayConsensusLoadingScreen: true,
|
||||
authorities: [] as Types.Address[],
|
||||
authoritySetId: null,
|
||||
});
|
||||
}
|
||||
|
||||
public unsubscribeConsensus(chain: Types.ChainLabel) {
|
||||
setHashData({ chain });
|
||||
this.resubscribeSendFinality = true;
|
||||
this.socket.send(`no-more-finality:${chain}`);
|
||||
}
|
||||
@@ -98,7 +116,7 @@ export class Connection {
|
||||
const { nodes, chains } = this.state;
|
||||
const ref = nodes.ref();
|
||||
|
||||
const updateState = (state: any) => { this.state = this.update(state); };
|
||||
const updateState: UpdateBound = (state) => { this.state = this.update(state); };
|
||||
const getState = () => this.state;
|
||||
const afg = new AfgHandling(updateState, getState);
|
||||
|
||||
@@ -217,6 +235,7 @@ export class Connection {
|
||||
if (this.state.subscribed === message.payload) {
|
||||
nodes.clear();
|
||||
this.state = this.update({ subscribed: null, nodes, chains });
|
||||
this.resetConsensus();
|
||||
}
|
||||
|
||||
break;
|
||||
@@ -248,21 +267,24 @@ export class Connection {
|
||||
|
||||
case Actions.AfgFinalized: {
|
||||
const [nodeAddress, finalizedNumber, finalizedHash] = message.payload;
|
||||
afg.receivedFinalized( nodeAddress, finalizedNumber, finalizedHash);
|
||||
const no = parseInt(String(finalizedNumber), 10) as Types.BlockNumber;
|
||||
afg.receivedFinalized( nodeAddress, no, finalizedHash);
|
||||
|
||||
break;
|
||||
}
|
||||
|
||||
case Actions.AfgReceivedPrevote: {
|
||||
const [nodeAddress, blockNumber, blockHash, voter] = message.payload;
|
||||
afg.receivedPre(nodeAddress, blockNumber, blockHash, voter, "prevote");
|
||||
const no = parseInt(String(blockNumber), 10) as Types.BlockNumber;
|
||||
afg.receivedPre(nodeAddress, no, blockHash, voter, "prevote");
|
||||
|
||||
break;
|
||||
}
|
||||
|
||||
case Actions.AfgReceivedPrecommit: {
|
||||
const [nodeAddress, blockNumber, blockHash, voter] = message.payload;
|
||||
afg.receivedPre(nodeAddress, blockNumber, blockHash, voter, "precommit");
|
||||
const no = parseInt(String(blockNumber), 10) as Types.BlockNumber;
|
||||
afg.receivedPre(nodeAddress, no, blockHash, voter, "precommit");
|
||||
|
||||
break;
|
||||
}
|
||||
@@ -393,6 +415,7 @@ export class Connection {
|
||||
|
||||
private handleDisconnect = async () => {
|
||||
this.state = this.update({ status: 'offline' });
|
||||
this.resetConsensus();
|
||||
this.clean();
|
||||
this.socket.close();
|
||||
this.socket = await Connection.socket();
|
||||
|
||||
@@ -56,6 +56,13 @@ export class Chain extends React.Component<Chain.Props, Chain.State> {
|
||||
};
|
||||
}
|
||||
|
||||
public shouldComponentUpdate(nextProps: Chain.Props, nextState: Chain.State): boolean {
|
||||
if (nextProps.appState.tabChanged === true && nextState.display === 'consensus') {
|
||||
this.setDisplay('list');
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
public render() {
|
||||
const { appState } = this.props;
|
||||
const { best, finalized, blockTimestamp, blockAverage } = appState;
|
||||
|
||||
@@ -61,5 +61,6 @@ export class Chains extends React.Component<Chains.Props, {}> {
|
||||
const connection = await this.props.connection;
|
||||
|
||||
connection.subscribe(chain);
|
||||
connection.resetConsensus();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,30 +1,30 @@
|
||||
.ConsensusList {
|
||||
.Consensus .ConsensusList {
|
||||
opacity: 0.0; /* the box should only show up once flexing has been applied */
|
||||
}
|
||||
|
||||
.ConsensusList table {
|
||||
.Consensus .ConsensusList table {
|
||||
border-spacing: 0px;
|
||||
}
|
||||
|
||||
.flexContainerLargeRow {
|
||||
.Consensus .flexContainerLargeRow {
|
||||
display: flex;
|
||||
align-items: stretch;
|
||||
flex-direction: row;
|
||||
opacity: 1.0;
|
||||
}
|
||||
|
||||
.flexContainerLargeRow .firstInRow {
|
||||
.Consensus .flexContainerLargeRow .firstInRow {
|
||||
width: 100%
|
||||
}
|
||||
|
||||
.flexContainerLargeRow .firstInRow .emptylegend,
|
||||
.flexContainerLargeRow .firstInRow .nameLegend {
|
||||
.Consensus .flexContainerLargeRow .firstInRow .emptylegend,
|
||||
.Consensus .flexContainerLargeRow .firstInRow .nameLegend {
|
||||
width: 99%;
|
||||
flex-grow: 1000000000;
|
||||
align-self: stretch;
|
||||
}
|
||||
|
||||
.flexContainerSmallRow {
|
||||
.Consensus .flexContainerSmallRow {
|
||||
display: flex;
|
||||
align-items: stretch;
|
||||
flex-direction: row;
|
||||
@@ -32,20 +32,20 @@
|
||||
opacity: 1.0;
|
||||
}
|
||||
|
||||
.flexContainerSmallRow div {
|
||||
.Consensus .flexContainerSmallRow div {
|
||||
align-self: stretch;
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.flexContainerSmallRow table .legend {
|
||||
.Consensus .flexContainerSmallRow table .legend {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.ConsensusList {
|
||||
.Consensus .ConsensusList {
|
||||
margin-bottom: 2px;
|
||||
}
|
||||
|
||||
.allRows {
|
||||
.Consensus {
|
||||
width: 100%;
|
||||
min-width: 1350px;
|
||||
min-height: 100%;
|
||||
@@ -54,91 +54,114 @@
|
||||
left: 0px;
|
||||
}
|
||||
|
||||
.SmallRow {
|
||||
.Consensus .SmallRow {
|
||||
float: left;
|
||||
clear: both;
|
||||
font-size: 8px !important;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.SmallRow svg {
|
||||
.Consensus .SmallRow svg {
|
||||
width: 14px;
|
||||
height: 14px;
|
||||
}
|
||||
|
||||
.hatching svg {
|
||||
.Consensus .hatching svg {
|
||||
width: 12px !important;
|
||||
height: 12px !important;
|
||||
}
|
||||
|
||||
.SmallRow .hatching svg {
|
||||
.Consensus .SmallRow .hatching svg {
|
||||
width: 10px !important;
|
||||
height: 10px !important;
|
||||
}
|
||||
|
||||
.matrixXLegend .Tooltip-container {
|
||||
.Consensus .matrixXLegend .Tooltip-container {
|
||||
height: auto !important;
|
||||
}
|
||||
|
||||
.legend {
|
||||
.Consensus .legend {
|
||||
text-align: center !important;
|
||||
}
|
||||
|
||||
.nameLegend {
|
||||
.Consensus .nameLegend {
|
||||
border-right: none;
|
||||
border-bottom: 1px dotted #555;
|
||||
}
|
||||
|
||||
.SmallRow .nameLegend {
|
||||
.Consensus .SmallRow .nameLegend {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.SmallRow .finalizedInfo .Tooltip-container {
|
||||
.Consensus .SmallRow .finalizedInfo .Tooltip-container {
|
||||
float: none;
|
||||
display: inline-block !important;
|
||||
vertical-align: middle;
|
||||
}
|
||||
|
||||
.SmallRow .finalizedInfo {
|
||||
.Consensus .SmallRow .finalizedInfo {
|
||||
min-height: 40px;
|
||||
min-width: 40px;
|
||||
}
|
||||
|
||||
.SmallRow .explicit,
|
||||
.SmallRow .implicit {
|
||||
.Consensus .SmallRow .explicit,
|
||||
.Consensus .SmallRow .implicit {
|
||||
height: 12px;
|
||||
}
|
||||
|
||||
.SmallRow .finalizedInfo .explicit,
|
||||
.SmallRow .finalizedInfo .implicit {
|
||||
.Consensus .SmallRow .finalizedInfo .explicit,
|
||||
.Consensus .SmallRow .finalizedInfo .implicit {
|
||||
margin-right: 6px;
|
||||
}
|
||||
|
||||
.nodeAddress {
|
||||
.Consensus .nodeAddress {
|
||||
margin-top: 4px;
|
||||
}
|
||||
|
||||
.first_false .legend .nodeAddress,
|
||||
.SmallRow .legend .nodeAddress,
|
||||
th.finalizedInfo .Tooltip-container {
|
||||
.Consensus .first_false .legend .nodeAddress,
|
||||
.Consensus .SmallRow .legend .nodeAddress,
|
||||
.Consensus th.finalizedInfo .Tooltip-container {
|
||||
float: none !important;
|
||||
text-align: center !important;
|
||||
}
|
||||
|
||||
.noStretchOnLastRow::after {
|
||||
.Consensus .noStretchOnLastRow::after {
|
||||
content: '';
|
||||
flex-grow: 1000000000;
|
||||
}
|
||||
|
||||
.flexContainerLargeRow .noStretchOnLastRow .firstInRow table {
|
||||
.Consensus .flexContainerLargeRow .noStretchOnLastRow .firstInRow table {
|
||||
width: auto !important;
|
||||
}
|
||||
|
||||
.flexContainerLargeRow .noStretchOnLastRow .firstInRow .emptylegend {
|
||||
.Consensus .flexContainerLargeRow .noStretchOnLastRow .firstInRow .emptylegend {
|
||||
width: auto !important;
|
||||
}
|
||||
|
||||
.flexContainerLargeRow .noStretchOnLastRow .firstInRow {
|
||||
.Consensus .flexContainerLargeRow .noStretchOnLastRow .firstInRow {
|
||||
width: auto !important;
|
||||
}
|
||||
|
||||
/* similar to .App-no-telemetry */
|
||||
.Consensus .noData {
|
||||
width: 100vw;
|
||||
line-height: 60vh;
|
||||
font-size: 56px;
|
||||
font-weight: 100;
|
||||
text-align: center;
|
||||
color: #888;
|
||||
}
|
||||
|
||||
/* similar to .App-no-telemetry */
|
||||
.Consensus .tooManyAuthorities {
|
||||
width: 100vw;
|
||||
line-height: 20vh;
|
||||
font-size: 56px;
|
||||
font-weight: 100;
|
||||
text-align: center;
|
||||
color: #888;
|
||||
}
|
||||
|
||||
.Consensus svg {
|
||||
z-index: 999999999;
|
||||
}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import * as React from 'react';
|
||||
import { Types } from '@dotstats/common';
|
||||
import { Types, Maybe } from '@dotstats/common';
|
||||
import { Connection } from '../../Connection';
|
||||
import Measure, {BoundingRect, ContentRect} from 'react-measure';
|
||||
|
||||
@@ -8,6 +8,8 @@ import { State as AppState } from '../../state';
|
||||
|
||||
import './Consensus.css';
|
||||
|
||||
const AUTHORITIES_LIMIT = 10;
|
||||
|
||||
export namespace Consensus {
|
||||
export interface Props {
|
||||
appState: Readonly<AppState>;
|
||||
@@ -26,6 +28,7 @@ export namespace Consensus {
|
||||
smallBlocksRows: number,
|
||||
countBlocksInSmallRow: number,
|
||||
smallRowsAddFlexClass: boolean,
|
||||
lastConsensusInfo: string,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -43,8 +46,56 @@ export class Consensus extends React.Component<Consensus.Props, {}> {
|
||||
smallBlocksRows: 1,
|
||||
countBlocksInSmallRow: 1,
|
||||
smallRowsAddFlexClass: false,
|
||||
lastConsensusInfo: "",
|
||||
};
|
||||
|
||||
public shouldComponentUpdate(nextProps: Consensus.Props, nextState: Consensus.State): boolean {
|
||||
if (this.props.appState.authorities.length === 0 && nextProps.appState.authorities.length === 0) {
|
||||
return false;
|
||||
}
|
||||
|
||||
this.calculateBoxCount(false);
|
||||
|
||||
// size detected, but flex class has not yet been added
|
||||
const largeBlocksSizeDetected = this.largeBlocksSizeDetected(nextState) === true &&
|
||||
this.state.largeRowsAddFlexClass === false;
|
||||
if (largeBlocksSizeDetected) {
|
||||
return true;
|
||||
}
|
||||
|
||||
const smallBlocksSizeDetected = this.smallBlocksSizeDetected(nextState) === true &&
|
||||
this.state.smallRowsAddFlexClass === false;
|
||||
if (smallBlocksSizeDetected) {
|
||||
return true;
|
||||
}
|
||||
|
||||
const windowSizeChanged = JSON.stringify(this.state.dimensions) !==
|
||||
JSON.stringify(nextState.dimensions);
|
||||
if (windowSizeChanged) {
|
||||
return true;
|
||||
}
|
||||
|
||||
const newConsensusInfoAvailable = this.state.lastConsensusInfo !==
|
||||
JSON.stringify(nextProps.appState.consensusInfo);
|
||||
if (newConsensusInfoAvailable) {
|
||||
return true;
|
||||
}
|
||||
|
||||
const authoritySetIdDidChange = this.props.appState.authoritySetId !==
|
||||
nextProps.appState.authoritySetId;
|
||||
if (authoritySetIdDidChange) {
|
||||
return true;
|
||||
}
|
||||
|
||||
const authoritiesDidChange = JSON.stringify(this.props.appState.authorities) !==
|
||||
JSON.stringify(nextProps.appState.authorities);
|
||||
if (authoritiesDidChange) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
public componentDidMount() {
|
||||
if (this.props.appState.subscribed != null) {
|
||||
const chain = this.props.appState.subscribed;
|
||||
@@ -60,9 +111,12 @@ export class Consensus extends React.Component<Consensus.Props, {}> {
|
||||
}
|
||||
|
||||
public largeBlocksSizeDetected(state: Consensus.State): boolean {
|
||||
const countBlocks = Object.keys(this.props.appState.consensusInfo).length;
|
||||
if (countBlocks === 1) {
|
||||
return state.largeBlockWithLegend.width > -1 && state.largeBlockWithLegend.height > -1;
|
||||
// we can only state that we detected the two block sizes (with
|
||||
// legend and without) if at least two blocks have been added:
|
||||
// the first displayed block will always have a legend with the
|
||||
// node names attached, the second not.
|
||||
if (this.props.appState.consensusInfo.length < 2) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// if there is more than one block then the size of the first block (with legend)
|
||||
@@ -108,10 +162,31 @@ export class Consensus extends React.Component<Consensus.Props, {}> {
|
||||
}
|
||||
|
||||
public render() {
|
||||
this.calculateBoxCount(false);
|
||||
|
||||
this.state.lastConsensusInfo = JSON.stringify(this.props.appState.consensusInfo);
|
||||
const lastBlocks = this.props.appState.consensusInfo;
|
||||
|
||||
if (this.props.appState.authorities.length > AUTHORITIES_LIMIT) {
|
||||
return <div className="Consensus">
|
||||
<div className="tooManyAuthorities">
|
||||
<p>
|
||||
Too many authorities.</p>
|
||||
<p>
|
||||
Won't display for more than {AUTHORITIES_LIMIT} authorities
|
||||
to protect your browser.
|
||||
</p>
|
||||
</div>;
|
||||
</div>;
|
||||
}
|
||||
|
||||
if (this.props.appState.displayConsensusLoadingScreen && lastBlocks.length < 2) {
|
||||
return <div className="Consensus">
|
||||
<div className="noData">
|
||||
{lastBlocks.length === 0 ? "No " : "Not yet enough "}
|
||||
GRANDPA data received by the authorities…
|
||||
</div>;
|
||||
</div>;
|
||||
}
|
||||
|
||||
let from = 0;
|
||||
let to = this.state.countBlocksInLargeRow;
|
||||
const firstLargeRow = this.getLargeRow(lastBlocks.slice(from, to), 0);
|
||||
@@ -124,19 +199,24 @@ export class Consensus extends React.Component<Consensus.Props, {}> {
|
||||
to = to + (this.state.smallBlocksRows * this.state.countBlocksInSmallRow);
|
||||
const smallRow = this.getSmallRow(lastBlocks.slice(from, to));
|
||||
|
||||
return (
|
||||
<React.Fragment>
|
||||
<Measure bounds={true} onResize={this.handleOnResize}>
|
||||
{({ measureRef }) => (
|
||||
<div className="allRows" ref={measureRef}>
|
||||
{firstLargeRow}
|
||||
{secondLargeRow}
|
||||
{smallRow}
|
||||
</div>
|
||||
)}
|
||||
</Measure>
|
||||
</React.Fragment>
|
||||
);
|
||||
const get = (measureRef: Maybe<(ref: Element | null) => void>) =>
|
||||
<div className="Consensus" ref={measureRef} key="Consensus">
|
||||
{firstLargeRow}
|
||||
{secondLargeRow}
|
||||
{smallRow}
|
||||
</div>;
|
||||
|
||||
if (!(this.state.smallRowsAddFlexClass && this.state.largeRowsAddFlexClass)) {
|
||||
return (
|
||||
<React.Fragment>
|
||||
<Measure bounds={true} onResize={this.handleOnResize}>
|
||||
{({measureRef}) => get(measureRef)}
|
||||
</Measure>
|
||||
</React.Fragment>
|
||||
);
|
||||
} else {
|
||||
return (get(null));
|
||||
}
|
||||
}
|
||||
|
||||
private handleOnResize = (contentRect: ContentRect) => {
|
||||
@@ -187,6 +267,7 @@ export class Consensus extends React.Component<Consensus.Props, {}> {
|
||||
compact={false}
|
||||
key={height}
|
||||
height={height}
|
||||
measure={!this.state.largeRowsAddFlexClass}
|
||||
consensusView={consensusView}
|
||||
authorities={this.getAuthorities()}
|
||||
authoritySetId={this.props.appState.authoritySetId}
|
||||
@@ -227,6 +308,7 @@ export class Consensus extends React.Component<Consensus.Props, {}> {
|
||||
compact={true}
|
||||
key={height}
|
||||
height={height}
|
||||
measure={!this.state.smallRowsAddFlexClass}
|
||||
consensusView={consensusView}
|
||||
authorities={this.getAuthorities()}
|
||||
authoritySetId={this.props.appState.authoritySetId} />;
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
.BlockConsensusMatrice {
|
||||
.Consensus .BlockConsensusMatrice {
|
||||
background-color: #222;
|
||||
font-family: monospace, sans-serif;
|
||||
border-spacing: 0px;
|
||||
@@ -6,144 +6,144 @@
|
||||
border-bottom: 1px solid #999;
|
||||
}
|
||||
|
||||
.LargeRow .BlockConsensusMatrice:last-child {
|
||||
.Consensus .LargeRow .BlockConsensusMatrice:last-child {
|
||||
border-right: none;
|
||||
}
|
||||
|
||||
.SmallRow .lastInRow {
|
||||
.Consensus .SmallRow .lastInRow {
|
||||
clear: right;
|
||||
width: 99%;
|
||||
page-break-after: always;
|
||||
}
|
||||
|
||||
.BlockConsensusMatrice th {
|
||||
.Consensus .BlockConsensusMatrice th {
|
||||
font-weight: normal;
|
||||
border-bottom: 1px dashed #999;
|
||||
}
|
||||
|
||||
.finalizedInfo, .legend {
|
||||
.Consensus .finalizedInfo, .legend {
|
||||
border-bottom: 1px dotted #555555;
|
||||
}
|
||||
|
||||
.finalizedInfo {
|
||||
.Consensus .finalizedInfo {
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.finalizedInfo .Tooltip-container {
|
||||
.Consensus .finalizedInfo .Tooltip-container {
|
||||
display: inline-block;
|
||||
white-space: nowrap;
|
||||
vertical-align: middle;
|
||||
}
|
||||
|
||||
.BlockConsensusMatrice .matrice {
|
||||
.Consensus .BlockConsensusMatrice .matrice {
|
||||
width: 28px;
|
||||
height: 28px;
|
||||
}
|
||||
|
||||
.BlockConsensusMatrice .matrice {
|
||||
.Consensus .BlockConsensusMatrice .matrice {
|
||||
font-weight: normal;
|
||||
border-right: 1px dotted #555555;
|
||||
border-bottom: 1px dotted #555;
|
||||
}
|
||||
|
||||
.BlockConsensusMatrice tr .matrice:last-child {
|
||||
.Consensus .BlockConsensusMatrice tr .matrice:last-child {
|
||||
border-right: none;
|
||||
}
|
||||
|
||||
.BlockConsensusMatrice .matrixXLegend {
|
||||
.Consensus .BlockConsensusMatrice .matrixXLegend {
|
||||
text-align: center;
|
||||
border-right: 1px dotted #555555;
|
||||
}
|
||||
|
||||
.BlockConsensusMatrice .matrixXLegend:last-child {
|
||||
.Consensus .BlockConsensusMatrice .matrixXLegend:last-child {
|
||||
border-right: none;
|
||||
}
|
||||
|
||||
.matrice {
|
||||
.Consensus .matrice {
|
||||
text-align: center !important;
|
||||
min-width: 35px;
|
||||
}
|
||||
|
||||
.SmallRow .matrixXLegend,
|
||||
.SmallRow .matrice {
|
||||
.Consensus .SmallRow .matrixXLegend,
|
||||
.Consensus .SmallRow .matrice {
|
||||
min-width: 26px;
|
||||
min-height: 26px;
|
||||
}
|
||||
|
||||
.finalizedInfo {
|
||||
.Consensus .finalizedInfo {
|
||||
text-align: center !important;
|
||||
}
|
||||
|
||||
.SmallRow .finalizedInfo {
|
||||
.Consensus .SmallRow .finalizedInfo {
|
||||
min-width: 40px;
|
||||
}
|
||||
|
||||
.finalizedInfo {
|
||||
.Consensus .finalizedInfo {
|
||||
text-align: right;
|
||||
border-right: 1px dashed #999;
|
||||
min-width: 48px;
|
||||
min-width: 50px;
|
||||
}
|
||||
|
||||
.finalizedInfo .Tooltip-container {
|
||||
.Consensus .finalizedInfo .Tooltip-container {
|
||||
float: none;
|
||||
}
|
||||
|
||||
.explicit {
|
||||
.Consensus .explicit {
|
||||
fill: #E70E81;
|
||||
}
|
||||
|
||||
.nodeName {
|
||||
.Consensus .nodeName {
|
||||
float: left;
|
||||
padding-right: 10px;
|
||||
padding-top: 4px;
|
||||
}
|
||||
|
||||
.flexContainerLargeRow .firstInRow .nodeContent {
|
||||
.Consensus .flexContainerLargeRow .firstInRow .nodeContent {
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.flexContainerLargeRow .firstInRow .nodeName {
|
||||
.Consensus .flexContainerLargeRow .firstInRow .nodeName {
|
||||
display: inline-block !important;
|
||||
float: none !important;
|
||||
vertical-align: middle;
|
||||
margin-bottom: 3px;
|
||||
}
|
||||
|
||||
.flexContainerLargeRow .firstInRow .nodeAddress {
|
||||
.Consensus .flexContainerLargeRow .firstInRow .nodeAddress {
|
||||
display: inline-block !important;
|
||||
float: none !important;
|
||||
vertical-align: middle;
|
||||
margin-right: 3px;
|
||||
}
|
||||
|
||||
.legend {
|
||||
.Consensus .legend {
|
||||
border-right: 1px solid #999;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.first_false .nodeName {
|
||||
.Consensus .first_false .nodeName {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.legend .nodeAddress {
|
||||
.Consensus .legend .nodeAddress {
|
||||
float: right;
|
||||
}
|
||||
|
||||
.Row {
|
||||
.Consensus .Row {
|
||||
color: #999;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.Row th, .Row td {
|
||||
.Consensus .Row th, .Consensus .Row td {
|
||||
text-align: left;
|
||||
padding: 2px;
|
||||
}
|
||||
|
||||
.Row td {
|
||||
.Consensus .Row td {
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.Row .Row-truncate {
|
||||
.Consensus .Row .Row-truncate {
|
||||
position: absolute;
|
||||
left: 0;
|
||||
right: 0;
|
||||
@@ -154,48 +154,49 @@
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
|
||||
.Row .Row-Tooltip {
|
||||
.Consensus .Row .Row-Tooltip {
|
||||
position: initial;
|
||||
padding: inherit;
|
||||
}
|
||||
|
||||
.Row:hover {
|
||||
.Consensus .Row:hover {
|
||||
background-color: #161616;
|
||||
}
|
||||
|
||||
.nodeAddress svg {
|
||||
.Consensus .nodeAddress svg {
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.nodeAddress svg:hover {
|
||||
.Consensus .nodeAddress svg:hover {
|
||||
transform: scale(2);
|
||||
}
|
||||
|
||||
.matrice .Icon ~ .Icon {
|
||||
.Consensus .matrice .Icon ~ .Icon {
|
||||
margin-left: -4px;
|
||||
}
|
||||
|
||||
.SmallRow .matrice .Prevote svg {
|
||||
.Consensus .SmallRow .matrice .Prevote svg {
|
||||
margin-left: 3px;
|
||||
margin-bottom: -11px;
|
||||
}
|
||||
|
||||
.SmallRow .matrice .Precommit svg {
|
||||
.Consensus .SmallRow .matrice .Precommit svg {
|
||||
margin-left: -1px;
|
||||
margin-top: -6px;
|
||||
margin-bottom: 0px;
|
||||
}
|
||||
|
||||
.jdenticonPlaceholder {
|
||||
.Consensus .jdenticonPlaceholder {
|
||||
width: 28px;
|
||||
float: right;
|
||||
}
|
||||
|
||||
.SmallRow .jdenticonPlaceholder {
|
||||
.Consensus .SmallRow .jdenticonPlaceholder {
|
||||
width: 14px;
|
||||
float: right;
|
||||
}
|
||||
|
||||
.even {
|
||||
.Consensus .even {
|
||||
background-color: #333;
|
||||
}
|
||||
|
||||
|
||||
@@ -16,23 +16,47 @@ import './ConsensusBlock.css';
|
||||
export namespace ConsensusBlock {
|
||||
export interface Props {
|
||||
authorities: Types.Authority[];
|
||||
authoritySetId: Types.AuthoritySetId;
|
||||
authoritySetId: Maybe<Types.AuthoritySetId>;
|
||||
height: Types.BlockNumber;
|
||||
firstInRow: boolean;
|
||||
lastInRow: boolean;
|
||||
compact: boolean;
|
||||
measure: boolean;
|
||||
consensusView: Types.ConsensusView;
|
||||
changeBlocks: (first: boolean, boundsRect: BoundingRect) => void;
|
||||
}
|
||||
}
|
||||
|
||||
export class ConsensusBlock extends React.Component<ConsensusBlock.Props, {}> {
|
||||
public state = {
|
||||
lastConsensusView: "",
|
||||
};
|
||||
|
||||
public shouldComponentUpdate(nextProps: ConsensusBlock.Props): boolean {
|
||||
if (this.props.authorities.length === 0 && nextProps.authorities.length === 0) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const positionInfoChanged = this.props.firstInRow !== nextProps.firstInRow ||
|
||||
this.props.lastInRow !== nextProps.lastInRow;
|
||||
if (positionInfoChanged) {
|
||||
return true;
|
||||
}
|
||||
|
||||
const newConsensusInfo =
|
||||
JSON.stringify(nextProps.consensusView) !== this.state.lastConsensusView;
|
||||
if (newConsensusInfo) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
public render() {
|
||||
this.state.lastConsensusView = JSON.stringify(this.props.consensusView);
|
||||
const finalizedByWhom = this.props.authorities.filter(authority => this.isFinalized(authority));
|
||||
|
||||
const ratio = finalizedByWhom.length + '/' + this.props.authorities.length;
|
||||
const tooltip = `${ratio} authorities finalized this block. Authority Set Id: ${this.props.authoritySetId}.`;
|
||||
let titleFinal = <span>{ratio}</span>;
|
||||
|
||||
const majorityFinalized = finalizedByWhom.length / this.props.authorities.length >= 2/3;
|
||||
@@ -40,48 +64,59 @@ export class ConsensusBlock extends React.Component<ConsensusBlock.Props, {}> {
|
||||
titleFinal = <span>FINAL</span>;
|
||||
} else if (majorityFinalized && this.props.compact) {
|
||||
const hash = this.getFinalizedHash(finalizedByWhom[0]);
|
||||
titleFinal =
|
||||
<Tooltip text={'Block hash: ' + hash ? String(hash) : ''} copy={true}>
|
||||
<Jdenticon hash={hash ? String(hash) : ''} size={this.props.compact ? '14px' : '28px'}/>
|
||||
</Tooltip>;
|
||||
titleFinal = <Jdenticon hash={hash ? String(hash) : ''} size={this.props.compact ? '14px' : '28px'}/>
|
||||
}
|
||||
|
||||
const handleOnResize = (contentRect: ContentRect) => {
|
||||
this.props.changeBlocks(this.props.firstInRow, contentRect.bounds as BoundingRect);
|
||||
};
|
||||
|
||||
return (<Measure bounds={true} onResize={handleOnResize}>{({ measureRef }) => (
|
||||
<div
|
||||
className={`BlockConsensusMatrice ${this.props.firstInRow ? 'firstInRow' : ''} ${this.props.lastInRow ? 'lastInRow' : ''}`}
|
||||
const get = (measureRef: Maybe<(ref: Element | null) => void>) => {
|
||||
return <div
|
||||
className={
|
||||
`BlockConsensusMatrice
|
||||
${this.props.firstInRow ? 'firstInRow' : ''} ${this.props.lastInRow ? 'lastInRow' : ''}`
|
||||
}
|
||||
key={'block_' + this.props.height}>
|
||||
<table ref={measureRef}>
|
||||
<thead>
|
||||
<tr className="Row">
|
||||
{this.props.firstInRow && !this.props.compact ?
|
||||
<th className="emptylegend"> </th> : ''}
|
||||
<th className="legend">
|
||||
<Tooltip text={`Block number: ${this.props.height}`}>
|
||||
{this.displayBlockNumber()}
|
||||
</Tooltip>
|
||||
</th>
|
||||
<th className='finalizedInfo'>
|
||||
<Tooltip text={tooltip}>{titleFinal}</Tooltip>
|
||||
</th>
|
||||
{this.props.authorities.map(authority =>
|
||||
<th
|
||||
className="matrixXLegend"
|
||||
key={`${this.props.height}_matrice_x_${authority.Address}`}>
|
||||
{this.getAuthorityContent(authority)}
|
||||
</th>)}
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<table ref={measureRef} key={'block_table_' + this.props.height}>
|
||||
<thead key={'block_thead_' + this.props.height}>
|
||||
<tr className="Row" key={'block_row_' + this.props.height}>
|
||||
{this.props.firstInRow && !this.props.compact ?
|
||||
<th className="emptylegend" key={'block_row_' + this.props.height + '_empty'}> </th> : null}
|
||||
<th className="legend" key={'block_row_' + this.props.height + '_legend'}>
|
||||
<Tooltip text={`Block number: ${this.props.height}`}>
|
||||
{this.displayBlockNumber()}
|
||||
</Tooltip>
|
||||
</th>
|
||||
<th className='finalizedInfo' key={'block_row_' + this.props.height + '_finalized_info'}>
|
||||
{titleFinal}
|
||||
</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>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>)}
|
||||
</Measure>);
|
||||
</div>
|
||||
};
|
||||
|
||||
if (this.props.measure) {
|
||||
return (
|
||||
<Measure bounds={true} onResize={handleOnResize}>{({measureRef}) => (
|
||||
get(measureRef)
|
||||
)}
|
||||
</Measure>
|
||||
);
|
||||
} else {
|
||||
return (get(null));
|
||||
}
|
||||
}
|
||||
|
||||
private displayBlockNumber(): string {
|
||||
@@ -111,88 +146,54 @@ export class ConsensusBlock extends React.Component<ConsensusBlock.Props, {}> {
|
||||
}
|
||||
|
||||
private renderMatriceRow(authority: Types.Authority, authorities: Types.Authority[], row: number): JSX.Element {
|
||||
let finalizedInfo = <Tooltip text="No information available yet."> </Tooltip>;
|
||||
let finalizedInfo = <span> </span>;
|
||||
let finalizedHash;
|
||||
|
||||
if (authority.NodeId != null && this.isFinalized(authority)) {
|
||||
const matrice = this.props.consensusView[authority.Address][authority.Address];
|
||||
|
||||
finalizedInfo = matrice.ImplicitFinalized ?
|
||||
<Tooltip text={`${authority.Name} finalized this block in ${matrice.ImplicitPointer}`}>
|
||||
<Icon className="implicit" src={finalizedIcon} alt="" />
|
||||
</Tooltip>
|
||||
:
|
||||
<Tooltip text={`${authority.Name} finalized this block in this block`}>
|
||||
<Icon className="explicit" src={finalizedIcon} alt="" />
|
||||
</Tooltip>
|
||||
<Icon className="implicit" src={finalizedIcon} alt="" /> :
|
||||
<Icon className="explicit" src={finalizedIcon} alt="" />;
|
||||
|
||||
finalizedHash = matrice.FinalizedHash ?
|
||||
<Tooltip text={`Block hash: ${matrice.FinalizedHash}`} copy={true}>
|
||||
<Jdenticon hash={matrice.FinalizedHash} size="28px"/>
|
||||
</Tooltip> : <div className="jdenticonPlaceholder"> </div>;
|
||||
<Jdenticon hash={matrice.FinalizedHash} size="28px"/> :
|
||||
<div className="jdenticonPlaceholder"> </div>;
|
||||
}
|
||||
|
||||
const name = authority.Name ? <span>{authority.Name}</span> : <em>no name received yet</em>;
|
||||
const firstName = this.props.firstInRow ? <td className="nameLegend">{name}</td> : '';
|
||||
const name = authority.Name ?
|
||||
<span>{authority.Name}</span> : <em>no data received from node</em>;
|
||||
const firstName = this.props.firstInRow ?
|
||||
<td key={"name_" + name} className="nameLegend">{name}</td> : '';
|
||||
|
||||
return <tr className="Row">
|
||||
return <tr className="Row" key={'block_row_' + this.props.height + '_' + row}>
|
||||
{firstName}
|
||||
<td className="legend">{this.getAuthorityContent(authority)}</td>
|
||||
<td className="finalizedInfo">{finalizedInfo}{finalizedHash}</td>
|
||||
<td className="legend" 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';
|
||||
return <td key={'matrice_' + authority.Address + '_' + columnNode.Address}
|
||||
className={`matrice ${evenOdd}`}>{this.getMatriceContent(authority, columnNode)}</td>
|
||||
return <td key={'matrice_' + this.props.height + '_' + row + '_' + authority.Address + '_' + columnNode.Address}
|
||||
className={`matrice ${evenOdd}`}>{this.getCellContent(authority, columnNode)}</td>
|
||||
})
|
||||
}
|
||||
</tr>;
|
||||
}
|
||||
|
||||
private getAuthorityContent(authority: Types.Authority): JSX.Element {
|
||||
return <div className="nodeContent">
|
||||
<div className="nodeAddress">
|
||||
<Tooltip text={authority.Address} copy={true}>
|
||||
<Identicon account={authority.Address} size={this.props.compact ? 14 : 28} />
|
||||
</Tooltip>
|
||||
return <div className="nodeContent" key={'authority_' + this.props.height + '_' + authority.Address}>
|
||||
<div className="nodeAddress" key={'authority_' + authority.Address}>
|
||||
<Identicon account={authority.Address} size={this.props.compact ? 14 : 28} />
|
||||
</div>
|
||||
</div>;
|
||||
}
|
||||
|
||||
private format(consensusDetail: Types.ConsensusDetail): string {
|
||||
const txt = [];
|
||||
if (consensusDetail.Prevote) {
|
||||
txt.push('Prevote on this chain in this block');
|
||||
} else if (consensusDetail.ImplicitPrevote) {
|
||||
txt.push('Prevote on this chain in block ' + consensusDetail.ImplicitPointer);
|
||||
}
|
||||
if (consensusDetail.Precommit) {
|
||||
txt.push('Precommit on this chain in this block');
|
||||
} else if (consensusDetail.ImplicitPrecommit) {
|
||||
txt.push('Precommit on this chain in block ' + consensusDetail.ImplicitPointer);
|
||||
}
|
||||
if (consensusDetail.Finalized && consensusDetail.ImplicitFinalized) {
|
||||
txt.push('Finalized this chain in block ' + consensusDetail.ImplicitPointer);
|
||||
} else if (consensusDetail.Finalized && !consensusDetail.ImplicitFinalized) {
|
||||
txt.push('Finalized this chain in this block');
|
||||
}
|
||||
return txt.join(', '); // + JSON.stringify((consensusDetail));
|
||||
}
|
||||
|
||||
private getMatriceContent(rowAuthority: Types.Authority, columnAuthority: Types.Authority) {
|
||||
private getCellContent(rowAuthority: Types.Authority, columnAuthority: Types.Authority) {
|
||||
const consensusInfo = this.props.consensusView &&
|
||||
rowAuthority.Address &&
|
||||
rowAuthority.Address in this.props.consensusView &&
|
||||
columnAuthority.Address in this.props.consensusView[rowAuthority.Address] ?
|
||||
this.props.consensusView[rowAuthority.Address][columnAuthority.Address] : null;
|
||||
|
||||
let tooltipText = consensusInfo ?
|
||||
rowAuthority.Name + ' has seen this of ' + columnAuthority.Name + ': ' +
|
||||
this.format(consensusInfo) : 'No information available yet.';
|
||||
|
||||
if (rowAuthority.Address === columnAuthority.Address) {
|
||||
tooltipText = 'Self-referential.';
|
||||
}
|
||||
this.props.consensusView[rowAuthority.Address][columnAuthority.Address] : null;
|
||||
|
||||
const prevote = consensusInfo && consensusInfo.Prevote;
|
||||
const implicitPrevote = consensusInfo && consensusInfo.ImplicitPrevote;
|
||||
@@ -218,12 +219,9 @@ export class ConsensusBlock extends React.Component<ConsensusBlock.Props, {}> {
|
||||
statPrecommit = <Icon src={checkIcon} className="explicit" alt="Precommit"/>;
|
||||
}
|
||||
|
||||
const stat = [statPrevote, statPrecommit];
|
||||
return <Tooltip text={tooltipText}>{stat}</Tooltip>
|
||||
return <span key={"icons_pre"}>{statPrevote}{statPrecommit}</span>;
|
||||
} else {
|
||||
return <Tooltip text={tooltipText}>
|
||||
<Icon src={hatchingIcon} className="hatching" alt=""/>
|
||||
</Tooltip>
|
||||
return <Icon src={hatchingIcon} className="hatching" alt=""/>;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -21,6 +21,6 @@ export class Icon extends React.Component<{}, Props> {
|
||||
public render() {
|
||||
const { alt, className, onClick, src } = this.props;
|
||||
|
||||
return <ReactSVG title={alt} className={`Icon ${ className || '' }`} path={src} onClick={onClick} />;
|
||||
return <ReactSVG key={this.props.src} title={alt} className={`Icon ${ className || '' }`} path={src} onClick={onClick} />;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -194,8 +194,10 @@ export interface State {
|
||||
best: Types.BlockNumber;
|
||||
finalized: Types.BlockNumber;
|
||||
consensusInfo: Types.ConsensusInfo;
|
||||
displayConsensusLoadingScreen: boolean;
|
||||
tabChanged: boolean;
|
||||
authorities: Types.Address[];
|
||||
authoritySetId: Types.AuthoritySetId;
|
||||
authoritySetId: Maybe<Types.AuthoritySetId>;
|
||||
sendFinality: boolean;
|
||||
blockTimestamp: Types.Timestamp;
|
||||
blockAverage: Maybe<Types.Milliseconds>;
|
||||
@@ -208,4 +210,4 @@ export interface State {
|
||||
}
|
||||
|
||||
export type Update = <K extends keyof State>(changes: Pick<State, K> | null) => Readonly<State>;
|
||||
|
||||
export type UpdateBound = <K extends keyof State>(changes: Pick<State, K> | null) => void;
|
||||
|
||||
Reference in New Issue
Block a user