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.props.authorities.map(authority => - )} - - - +
  - - {this.displayBlockNumber()} - - - {titleFinal} - - {this.getAuthorityContent(authority)} -
+ + + {this.props.firstInRow && !this.props.compact ? + : null} + + + {this.props.authorities.map(authority => + )} + + + {this.props.authorities.map((authority, row) => this.renderMatriceRow(authority, this.props.authorities, row))} - +
  + + {this.displayBlockNumber()} + + + {titleFinal} + + {this.getAuthorityContent(authority)} +
-
)} - ); +
+ }; + + 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;