diff --git a/packages/backend/src/Node.ts b/packages/backend/src/Node.ts
index e3bea02..ccc42cf 100644
--- a/packages/backend/src/Node.ts
+++ b/packages/backend/src/Node.ts
@@ -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 {
diff --git a/packages/common/src/index.ts b/packages/common/src/index.ts
index 3a18da5..9556cda 100644
--- a/packages/common/src/index.ts
+++ b/packages/common/src/index.ts
@@ -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;
diff --git a/packages/common/src/types.ts b/packages/common/src/types.ts
index 5168362..1f264c7 100644
--- a/packages/common/src/types.ts
+++ b/packages/common/src/types.ts
@@ -40,7 +40,8 @@ export declare type Authority = {
export declare type Authorities = Array
;
export declare type AuthoritySetId = Opaque;
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;
export declare type ConsensusView = Map;
export declare type ConsensusState = Map;
export declare type ConsensusDetail = {
diff --git a/packages/frontend/src/AfgHandling.ts b/packages/frontend/src/AfgHandling.ts
index 143d9cf..33cd2c1 100644
--- a/packages/frontend/src/AfgHandling.ts
+++ b/packages/frontend/src/AfgHandling.ts
@@ -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;
constructor(
- updateState: (state: any) => void,
- getState: () => State,
+ updateState: UpdateBound,
+ getState: () => Readonly,
) {
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);
+
+ // 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);
+ 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);
+
+ // 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,
) {
- 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;
+ }
}
}
diff --git a/packages/frontend/src/App.tsx b/packages/frontend/src/App.tsx
index 8c7bf31..36fc4ee 100644
--- a/packages/frontend/src/App.tsx
+++ b/packages/frontend/src/App.tsx
@@ -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 (
- Waiting for telemetry data...
+ Waiting for telemetry…
);
}
@@ -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);
}
diff --git a/packages/frontend/src/Connection.ts b/packages/frontend/src/Connection.ts
index 921904d..bec2600 100644
--- a/packages/frontend/src/Connection.ts
+++ b/packages/frontend/src/Connection.ts
@@ -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 {
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();
diff --git a/packages/frontend/src/components/Chain/Chain.tsx b/packages/frontend/src/components/Chain/Chain.tsx
index 0c8db16..c08ca36 100644
--- a/packages/frontend/src/components/Chain/Chain.tsx
+++ b/packages/frontend/src/components/Chain/Chain.tsx
@@ -56,6 +56,13 @@ export class Chain extends React.Component {
};
}
+ 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;
diff --git a/packages/frontend/src/components/Chains.tsx b/packages/frontend/src/components/Chains.tsx
index 14444bc..7f29d13 100644
--- a/packages/frontend/src/components/Chains.tsx
+++ b/packages/frontend/src/components/Chains.tsx
@@ -61,5 +61,6 @@ export class Chains extends React.Component {
const connection = await this.props.connection;
connection.subscribe(chain);
+ connection.resetConsensus();
}
}
diff --git a/packages/frontend/src/components/Consensus/Consensus.css b/packages/frontend/src/components/Consensus/Consensus.css
index 0309150..6d256b1 100644
--- a/packages/frontend/src/components/Consensus/Consensus.css
+++ b/packages/frontend/src/components/Consensus/Consensus.css
@@ -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;
+}
diff --git a/packages/frontend/src/components/Consensus/Consensus.tsx b/packages/frontend/src/components/Consensus/Consensus.tsx
index 94c0dde..6747387 100644
--- a/packages/frontend/src/components/Consensus/Consensus.tsx
+++ b/packages/frontend/src/components/Consensus/Consensus.tsx
@@ -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;
@@ -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 {
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 {
}
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 {
}
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
+
+
+ Too many authorities.
+
+ Won't display for more than {AUTHORITIES_LIMIT} authorities
+ to protect your browser.
+
+
;
+
;
+ }
+
+ if (this.props.appState.displayConsensusLoadingScreen && lastBlocks.length < 2) {
+ return
+
+ {lastBlocks.length === 0 ? "No " : "Not yet enough "}
+ GRANDPA data received by the authorities…
+
;
+
;
+ }
+
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 {
to = to + (this.state.smallBlocksRows * this.state.countBlocksInSmallRow);
const smallRow = this.getSmallRow(lastBlocks.slice(from, to));
- return (
-
-
- {({ measureRef }) => (
-
- {firstLargeRow}
- {secondLargeRow}
- {smallRow}
-
- )}
-
-
- );
+ const get = (measureRef: Maybe<(ref: Element | null) => void>) =>
+
+ {firstLargeRow}
+ {secondLargeRow}
+ {smallRow}
+
;
+
+ if (!(this.state.smallRowsAddFlexClass && this.state.largeRowsAddFlexClass)) {
+ return (
+
+
+ {({measureRef}) => get(measureRef)}
+
+
+ );
+ } else {
+ return (get(null));
+ }
}
private handleOnResize = (contentRect: ContentRect) => {
@@ -187,6 +267,7 @@ export class Consensus extends React.Component {
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 {
compact={true}
key={height}
height={height}
+ measure={!this.state.smallRowsAddFlexClass}
consensusView={consensusView}
authorities={this.getAuthorities()}
authoritySetId={this.props.appState.authoritySetId} />;
diff --git a/packages/frontend/src/components/Consensus/ConsensusBlock.css b/packages/frontend/src/components/Consensus/ConsensusBlock.css
index 23e7f5e..b5cccf0 100644
--- a/packages/frontend/src/components/Consensus/ConsensusBlock.css
+++ b/packages/frontend/src/components/Consensus/ConsensusBlock.css
@@ -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;
}
diff --git a/packages/frontend/src/components/Consensus/ConsensusBlock.tsx b/packages/frontend/src/components/Consensus/ConsensusBlock.tsx
index 5172ec1..d2f22a2 100644
--- a/packages/frontend/src/components/Consensus/ConsensusBlock.tsx
+++ b/packages/frontend/src/components/Consensus/ConsensusBlock.tsx
@@ -16,23 +16,47 @@ import './ConsensusBlock.css';
export namespace ConsensusBlock {
export interface Props {
authorities: Types.Authority[];
- authoritySetId: Types.AuthoritySetId;
+ authoritySetId: Maybe;
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 {
+ 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 = {ratio};
const majorityFinalized = finalizedByWhom.length / this.props.authorities.length >= 2/3;
@@ -40,48 +64,59 @@ export class ConsensusBlock extends React.Component {
titleFinal = FINAL;
} else if (majorityFinalized && this.props.compact) {
const hash = this.getFinalizedHash(finalizedByWhom[0]);
- titleFinal =
-
-
- ;
+ titleFinal =
}
const handleOnResize = (contentRect: ContentRect) => {
this.props.changeBlocks(this.props.firstInRow, contentRect.bounds as BoundingRect);
};
- return ({({ measureRef }) => (
- void>) => {
+ return
-
-
-
- {this.props.firstInRow && !this.props.compact ?
- | | : ''}
-
-
- {this.displayBlockNumber()}
-
- |
-
- {titleFinal}
- |
- {this.props.authorities.map(authority =>
-
- {this.getAuthorityContent(authority)}
- | )}
-
-
-
+
+
+
+ {this.props.firstInRow && !this.props.compact ?
+ | | : null}
+
+
+ {this.displayBlockNumber()}
+
+ |
+
+ {titleFinal}
+ |
+ {this.props.authorities.map(authority =>
+
+ {this.getAuthorityContent(authority)}
+ | )}
+
+
+
{this.props.authorities.map((authority, row) =>
this.renderMatriceRow(authority, this.props.authorities, row))}
-
+
- )}
- );
+
+ };
+
+ if (this.props.measure) {
+ return (
+ {({measureRef}) => (
+ get(measureRef)
+ )}
+
+ );
+ } else {
+ return (get(null));
+ }
}
private displayBlockNumber(): string {
@@ -111,88 +146,54 @@ export class ConsensusBlock extends React.Component {
}
private renderMatriceRow(authority: Types.Authority, authorities: Types.Authority[], row: number): JSX.Element {
- let finalizedInfo = ;
+ let finalizedInfo = ;
let finalizedHash;
if (authority.NodeId != null && this.isFinalized(authority)) {
const matrice = this.props.consensusView[authority.Address][authority.Address];
finalizedInfo = matrice.ImplicitFinalized ?
-
-
-
- :
-
-
-
+ :
+ ;
finalizedHash = matrice.FinalizedHash ?
-
-
- :
;
+ :
+
;
}
- const name = authority.Name ? {authority.Name} : no name received yet;
- const firstName = this.props.firstInRow ? {name} | : '';
+ const name = authority.Name ?
+ {authority.Name} : no data received from node;
+ const firstName = this.props.firstInRow ?
+ {name} | : '';
- return
+ return
{firstName}
- | {this.getAuthorityContent(authority)} |
- {finalizedInfo}{finalizedHash} |
+ {this.getAuthorityContent(authority)} |
+ {finalizedInfo}{finalizedHash} |
{
authorities.map((columnNode, column) => {
const evenOdd = ((row % 2) + column) % 2 === 0 ? 'even' : 'odd';
- return {this.getMatriceContent(authority, columnNode)} |
+ return {this.getCellContent(authority, columnNode)} |
})
}
;
}
private getAuthorityContent(authority: Types.Authority): JSX.Element {
- return
-
-
-
-
+ return
;
}
- 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
{
statPrecommit = ;
}
- const stat = [statPrevote, statPrecommit];
- return {stat}
+ return {statPrevote}{statPrecommit};
} else {
- return
-
-
+ return ;
}
}
diff --git a/packages/frontend/src/components/Icon.tsx b/packages/frontend/src/components/Icon.tsx
index 8560b8f..fdde9a2 100644
--- a/packages/frontend/src/components/Icon.tsx
+++ b/packages/frontend/src/components/Icon.tsx
@@ -21,6 +21,6 @@ export class Icon extends React.Component<{}, Props> {
public render() {
const { alt, className, onClick, src } = this.props;
- return ;
+ return ;
}
}
diff --git a/packages/frontend/src/state.ts b/packages/frontend/src/state.ts
index d269d51..2b4c95f 100644
--- a/packages/frontend/src/state.ts
+++ b/packages/frontend/src/state.ts
@@ -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;
sendFinality: boolean;
blockTimestamp: Types.Timestamp;
blockAverage: Maybe;
@@ -208,4 +210,4 @@ export interface State {
}
export type Update = (changes: Pick | null) => Readonly;
-
+export type UpdateBound = (changes: Pick | null) => void;