Remove broken "Consensus" tab from UI and related code (#434)

* Remove consensus-tab related stuff, and unused messages, from the telemetry backend

* also remove AfgAuthoritySet feed message, and handle same from node

* Blat everything consensus related that I can find in the UI

* cargo fmt

* README: had -> has

Co-authored-by: David <dvdplm@gmail.com>

Co-authored-by: David <dvdplm@gmail.com>
This commit is contained in:
James Wilson
2021-11-25 12:34:36 +00:00
committed by GitHub
parent 73a4cf29b9
commit 17432d712f
26 changed files with 57 additions and 2021 deletions
-325
View File
@@ -1,325 +0,0 @@
// Source code for the Substrate Telemetry Server.
// Copyright (C) 2021 Parity Technologies (UK) Ltd.
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with this program. If not, see <https://www.gnu.org/licenses/>.
import { Types } from './common';
import { State, Update } from './state';
import { ConsensusDetail } from './common/types';
// Number of blocks which are kept in memory
const BLOCKS_LIMIT = 50;
export class AfgHandling {
constructor(
private readonly appUpdate: Update,
private readonly appState: Readonly<State>
) {
this.appUpdate = appUpdate;
this.appState = appState;
}
public receivedAuthoritySet(
authoritySetId: Types.AuthoritySetId,
authorities: Types.Authorities
) {
if (
this.appState.authoritySetId != null &&
authoritySetId !== this.appState.authoritySetId
) {
// the visualization is restarted when we receive a new authority set
this.appUpdate({
authoritySetId,
authorities,
consensusInfo: [],
displayConsensusLoadingScreen: false,
});
} else if (this.appState.authoritySetId == null) {
// initial display
this.appUpdate({
authoritySetId,
authorities,
consensusInfo: [],
displayConsensusLoadingScreen: true,
});
}
return null;
}
public receivedFinalized(
addr: Types.Address,
finalizedNumber: Types.BlockNumber,
finalizedHash: Types.BlockHash
) {
const state = this.appState;
if (finalizedNumber < state.best - BLOCKS_LIMIT) {
return;
}
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;
}
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(state.consensusInfo, finalizedNumber, op, addr, addr);
this.pruneBlocks(state.consensusInfo);
this.appUpdate({ consensusInfo: state.consensusInfo });
}
public receivedPre(
addr: Types.Address,
height: Types.BlockNumber,
voter: Types.Address,
what: string
) {
const state = this.appState;
if (height < state.best - BLOCKS_LIMIT) {
return;
}
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 = (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;
}
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;
};
const consensusInfo = this.appState.consensusInfo;
this.backfill(consensusInfo, height, op, addr, voter);
this.pruneBlocks(consensusInfo);
this.appUpdate({ consensusInfo });
}
// Initializes the `ConsensusView` with empty objects.
private initialiseConsensusView(
consensusInfo: Types.ConsensusInfo,
height: Types.BlockNumber,
own: Types.Address,
other: Types.Address
) {
const found = consensusInfo.find(([blockNumber]) => blockNumber === height);
let consensusView;
if (found) {
[, consensusView] = found;
this.initialiseConsensusViewByRef(consensusView, own, other);
} else {
consensusView = {} as Types.ConsensusView;
this.initialiseConsensusViewByRef(consensusView, own, other);
const item: Types.ConsensusItem = [height, consensusView];
const insertPos = consensusInfo.findIndex(
([elHeight]) => elHeight < height
);
if (insertPos >= 0) {
consensusInfo.splice(insertPos, 0, item);
} else {
consensusInfo.push(item);
}
}
}
// Initializes the `ConsensusView` with empty objects.
private initialiseConsensusViewByRef(
consensusView: Types.ConsensusView,
own: Types.Address,
other: Types.Address
) {
if (!consensusView[own]) {
consensusView[own] = {} as Types.ConsensusState;
}
if (!consensusView[own][other]) {
consensusView[own][other] = {} as Types.ConsensusDetail;
}
}
// Fill the block cache back from the `to` number to the last block.
// The function `f` is used to evaluate if we should continue backfilling.
// `f` returns false when backfilling the cache should be stopped, true to continue.
//
// Returns block number until which we backfilled.
private backfill(
consensusInfo: Types.ConsensusInfo,
start: Types.BlockNumber,
f: (i: Types.BlockNumber, index: number) => boolean,
own: Types.Address,
other: Types.Address
) {
// 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.appState.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;
}
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;
}
}
}
private updateConsensusInfo(
consensusInfo: Types.ConsensusInfo,
height: Types.BlockNumber,
addr: Types.Address,
voter: Types.Address,
data: Partial<Types.ConsensusDetail>
) {
const found = consensusInfo.findIndex(
([blockNumber]) => blockNumber === height
);
if (found < 0) {
return;
}
for (const k in data) {
if (data.hasOwnProperty(k)) {
consensusInfo[found][1][addr][voter][k] = data[k];
}
}
}
private pruneBlocks(consensusInfo: Types.ConsensusInfo) {
if (consensusInfo.length >= BLOCKS_LIMIT) {
consensusInfo.length = BLOCKS_LIMIT;
}
}
}
-5
View File
@@ -101,11 +101,6 @@ export default class App extends React.Component<{}, {}> {
status: 'offline',
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: null,
sendFinality: false,
blockTimestamp: 0 as Types.Timestamp,
blockAverage: null,
timeDiff: 0 as Types.Milliseconds,
+2 -67
View File
@@ -18,8 +18,6 @@ import { VERSION, timestamp, FeedMessage, Types, Maybe, sleep } from './common';
import { State, Update, Node, ChainData, PINNED_CHAINS } from './state';
import { PersistentSet } from './persist';
import { getHashData, setHashData } from './utils';
import { AfgHandling } from './AfgHandling';
import { VIS_AUTHORITIES_LIMIT } from './components/Consensus';
import { ACTIONS } from './common/feed';
import {
Column,
@@ -126,8 +124,6 @@ export class Connection {
private pingSent: Maybe<Types.Timestamp> = null;
// chain label to resubsribe to on reconnect
private resubscribeTo: Maybe<Types.GenesisHash> = getHashData().chain;
// flag whether or not FE should subscribe to consensus updates on reconnect
private resubscribeSendFinality: boolean = getHashData().tab === 'consensus';
constructor(
private socket: WebSocket,
@@ -154,35 +150,11 @@ export class Connection {
this.socket.send(`subscribe:${chain}`);
}
public subscribeConsensus(chain: Types.GenesisHash) {
if (this.appState.authorities.length <= VIS_AUTHORITIES_LIMIT) {
setHashData({ chain });
this.resubscribeSendFinality = true;
this.socket.send(`send-finality:${chain}`);
}
}
public resetConsensus() {
this.appUpdate({
consensusInfo: new Array() as Types.ConsensusInfo,
displayConsensusLoadingScreen: true,
authorities: [] as Types.Address[],
authoritySetId: null,
});
}
public unsubscribeConsensus(chain: Types.GenesisHash) {
this.resubscribeSendFinality = true;
this.socket.send(`no-more-finality:${chain}`);
}
private handleMessages = (messages: FeedMessage.Message[]) => {
this.messageTimeout?.reset();
const { nodes, chains, sortBy, selectedColumns } = this.appState;
const nodesStateRef = nodes.ref;
const afg = new AfgHandling(this.appUpdate, this.appState);
let sortByColumn: Maybe<Column> = null;
if (sortBy != null) {
@@ -361,7 +333,6 @@ export class Connection {
if (this.appState.subscribed === message.payload) {
nodes.clear();
this.appUpdate({ subscribed: null, nodes, chains });
this.resetConsensus();
}
break;
@@ -391,37 +362,6 @@ export class Connection {
break;
}
case ACTIONS.AfgFinalized: {
const [nodeAddress, finalizedNumber, finalizedHash] = message.payload;
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;
const no = parseInt(String(blockNumber), 10) as Types.BlockNumber;
afg.receivedPre(nodeAddress, no, voter, 'prevote');
break;
}
case ACTIONS.AfgReceivedPrecommit: {
const [nodeAddress, blockNumber, blockHash, voter] = message.payload;
const no = parseInt(String(blockNumber), 10) as Types.BlockNumber;
afg.receivedPre(nodeAddress, no, voter, 'precommit');
break;
}
case ACTIONS.AfgAuthoritySet: {
const [authoritySetId, authorities] = message.payload;
afg.receivedAuthoritySet(authoritySetId, authorities);
break;
}
default: {
break;
}
@@ -456,8 +396,7 @@ export class Connection {
if (this.appState.subscribed) {
this.resubscribeTo = this.appState.subscribed;
this.resubscribeSendFinality = this.appState.sendFinality;
this.appUpdate({ subscribed: null, sendFinality: false });
this.appUpdate({ subscribed: null });
}
this.socket.addEventListener('message', this.handleFeedData);
@@ -544,7 +483,7 @@ export class Connection {
private autoSubscribe() {
const { subscribed, chains } = this.appState;
const { resubscribeTo, resubscribeSendFinality } = this;
const { resubscribeTo } = this;
if (subscribed) {
return;
@@ -553,9 +492,6 @@ export class Connection {
if (resubscribeTo) {
if (chains.has(resubscribeTo)) {
this.subscribe(resubscribeTo);
if (resubscribeSendFinality) {
this.subscribeConsensus(resubscribeTo);
}
return;
}
}
@@ -581,7 +517,6 @@ export class Connection {
private handleDisconnect = async () => {
console.warn('Disconnecting; will attempt reconnect');
this.appUpdate({ status: 'offline' });
this.resetConsensus();
this.clean();
this.socket.close();
this.socket = await Connection.socket();
-15
View File
@@ -80,21 +80,6 @@ export declare type AuthoritySetInfo = [
BlockNumber,
BlockHash
];
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 interface ConsensusDetail {
Precommit: Precommit;
ImplicitPrecommit: ImplicitPrecommit;
Prevote: Prevote;
ImplicitPrevote: ImplicitPrevote;
ImplicitPointer: ImplicitPointer;
Finalized: ImplicitFinalized;
ImplicitFinalized: Finalized;
FinalizedHash: BlockHash;
FinalizedHeight: BlockNumber;
}
export declare type Precommit = Opaque<boolean, 'Precommit'>;
export declare type Prevote = Opaque<boolean, 'Prevote'>;
export declare type Finalized = Opaque<boolean, 'Finalized'>;
-1
View File
@@ -70,6 +70,5 @@ export class AllChains extends React.Component<AllChains.Props, {}> {
const connection = await this.props.connection;
connection.subscribe(chain);
connection.resetConsensus();
}
}
+1 -8
View File
@@ -20,7 +20,7 @@ import { Types, Maybe } from '../../common';
import { State as AppState, Update as AppUpdate } from '../../state';
import { getHashData } from '../../utils';
import { Header } from './';
import { Tile, Ago, List, Map, Settings, Consensus } from '../';
import { List, Map, Settings } from '../';
import { Persistent, PersistentObject, PersistentSet } from '../../persist';
import './Chain.css';
@@ -55,9 +55,6 @@ export class Chain extends React.Component<Chain.Props, Chain.State> {
case 'settings':
display = 'settings';
break;
case 'consensus':
display = 'consensus';
break;
}
this.state = {
@@ -96,10 +93,6 @@ export class Chain extends React.Component<Chain.Props, Chain.State> {
const { appState, appUpdate, connection, pins, sortBy } = this.props;
if (display === 'consensus') {
return <Consensus appState={appState} connection={connection} />;
}
return display === 'list' ? (
<List
appState={appState}
-8
View File
@@ -90,14 +90,6 @@ export class Header extends React.Component<Header.Props, {}> {
current={currentTab}
setDisplay={setDisplay}
/>
<Tab
icon={consensusIcon}
label="Consensus"
display="consensus"
tab="consensus"
current={currentTab}
setDisplay={setDisplay}
/>
<Tab
icon={settingsIcon}
label="Settings"
-1
View File
@@ -114,6 +114,5 @@ export class Chains extends React.Component<Chains.Props, {}> {
const connection = await this.props.connection;
connection.subscribe(chain);
connection.resetConsensus();
}
}
@@ -1,185 +0,0 @@
/*
Source code for the Substrate Telemetry Server.
Copyright (C) 2021 Parity Technologies (UK) Ltd.
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
.Consensus .ConsensusList {
opacity: 0; /* the box should only show up once flexing has been applied */
}
.Consensus .ConsensusList table {
border-spacing: 0px;
}
.Consensus .flexContainerLargeRow {
display: flex;
align-items: stretch;
flex-direction: row;
opacity: 1;
}
.Consensus .flexContainerLargeRow .firstInRow {
width: 100%;
}
.Consensus .flexContainerLargeRow .firstInRow .emptyLegend,
.Consensus .flexContainerLargeRow .firstInRow .nameLegend {
width: 99%;
flex-grow: 1000000000;
align-self: stretch;
}
.Consensus .flexContainerSmallRow {
display: flex;
align-items: stretch;
flex-direction: row;
flex-wrap: wrap;
opacity: 1;
}
.Consensus .flexContainerSmallRow div {
align-self: stretch;
flex: 1;
}
.Consensus .flexContainerSmallRow table .legend {
width: 100%;
}
.Consensus .ConsensusList {
margin-bottom: 2px;
}
.Consensus {
width: 100%;
min-width: 1350px;
min-height: 100%;
position: absolute;
top: 0px;
left: 0px;
}
.Consensus .SmallRow {
float: left;
clear: both;
font-size: 8px !important;
width: 100%;
}
.Consensus .SmallRow svg {
width: 14px;
height: 14px;
}
.Consensus .hatching svg {
width: 12px !important;
height: 12px !important;
}
.Consensus .SmallRow .hatching svg {
width: 10px !important;
height: 10px !important;
}
.Consensus .matrixXLegend .Tooltip-container {
height: auto !important;
}
.Consensus .legend {
text-align: center !important;
}
.Consensus .nameLegend {
border-right: none;
border-bottom: 1px dotted #555;
}
.Consensus .SmallRow .nameLegend {
display: none;
}
.Consensus .SmallRow .finalizedInfo .Tooltip-container {
float: none;
display: inline-block !important;
vertical-align: middle;
}
.Consensus .SmallRow .finalizedInfo {
min-height: 40px;
min-width: 40px;
}
.Consensus .SmallRow .explicit,
.Consensus .SmallRow .implicit {
height: 12px;
}
.Consensus .SmallRow .finalizedInfo .explicit,
.Consensus .SmallRow .finalizedInfo .implicit {
margin-right: 6px;
}
.Consensus .nodeAddress {
margin-top: 4px;
}
.Consensus .first_false .legend .nodeAddress,
.Consensus .SmallRow .legend .nodeAddress,
.Consensus th.finalizedInfo .Tooltip-container {
float: none !important;
text-align: center !important;
}
.Consensus .noStretchOnLastRow::after {
content: '';
flex-grow: 1000000000;
}
.Consensus .flexContainerLargeRow .noStretchOnLastRow .firstInRow table {
width: auto !important;
}
.Consensus .flexContainerLargeRow .noStretchOnLastRow .firstInRow .emptyLegend {
width: auto !important;
}
.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,439 +0,0 @@
// Source code for the Substrate Telemetry Server.
// Copyright (C) 2021 Parity Technologies (UK) Ltd.
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with this program. If not, see <https://www.gnu.org/licenses/>.
import * as React from 'react';
import { Types, Maybe } from '../../common';
import { Connection } from '../../Connection';
import Measure, { BoundingRect, ContentRect } from 'react-measure';
import { ConsensusBlock } from './';
import { State as AppState } from '../../state';
import './Consensus.css';
// Maximum number of authorities the visualization is
// allowed of processing.
export const VIS_AUTHORITIES_LIMIT = 10;
export namespace Consensus {
export interface Props {
appState: Readonly<AppState>;
connection: Promise<Connection>;
}
export interface State {
dimensions: BoundingRect;
largeBlockWithLegend: BoundingRect;
largeBlock: BoundingRect;
countBlocksInLargeRow: number;
largeRowsAddFlexClass: boolean;
smallBlock: BoundingRect;
smallBlocksRows: number;
countBlocksInSmallRow: number;
smallRowsAddFlexClass: boolean;
lastConsensusInfo: string;
}
}
export class Consensus extends React.Component<Consensus.Props, {}> {
public state = {
// entire area available for rendering the visualization
dimensions: { width: -1, height: -1 } as BoundingRect,
largeBlockWithLegend: { width: -1, height: -1 } as BoundingRect,
largeBlock: { width: -1, height: -1 } as BoundingRect,
countBlocksInLargeRow: 2,
largeRowsAddFlexClass: false,
smallBlock: { width: -1, height: -1 } as BoundingRect,
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;
this.subscribeConsensus(chain);
}
}
public componentWillUnmount() {
if (this.props.appState.subscribed != null) {
const chain = this.props.appState.subscribed;
this.unsubscribeConsensus(chain);
}
}
public largeBlocksSizeDetected(state: Consensus.State): boolean {
// 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)
// will be different from the succeeding blocks (without legend)
return (
state.largeBlockWithLegend.width > -1 &&
state.largeBlockWithLegend.height > -1 &&
state.largeBlock.width > -1 &&
state.largeBlock.height > -1
);
}
public smallBlocksSizeDetected(state: Consensus.State): boolean {
return (
state.smallBlock.width > -1 && state.largeBlockWithLegend.height > -1
);
}
public calculateBoxCount(wasResized: boolean) {
// if the css class for flexing has already been added we don't calculate
// any box measurements then, because the box sizes would be skewed then.
if (
(wasResized || this.state.largeRowsAddFlexClass === false) &&
this.largeBlocksSizeDetected(this.state)
) {
// we need to add +2 because of the last block which doesn't contain a border.
let countBlocks =
(this.state.dimensions.width -
this.state.largeBlockWithLegend.width +
2) /
(this.state.largeBlock.width + 2);
// +1 because the firstRect was subtracted above and needs to be counted back in.
// default count is 2 because we need two blocks to measure properly (one with legend
// and one without. these measures are necessary to calculate the number of blocks
// which fit.
countBlocks =
Math.floor(countBlocks + 1) < 1 ? 2 : Math.floor(countBlocks + 1);
this.setState({
largeRowsAddFlexClass: true,
countBlocksInLargeRow: countBlocks,
});
}
if (
(wasResized || this.state.smallRowsAddFlexClass === false) &&
this.smallBlocksSizeDetected(this.state)
) {
const howManyRows = 2;
const heightLeft =
this.state.dimensions.height -
this.state.largeBlock.height * howManyRows;
let smallBlocksRows = heightLeft / this.state.smallBlock.height;
smallBlocksRows = smallBlocksRows < 1 ? 1 : Math.floor(smallBlocksRows);
let countBlocksInSmallRow =
this.state.dimensions.width / this.state.smallBlock.width;
countBlocksInSmallRow =
countBlocksInSmallRow < 1 ? 1 : Math.floor(countBlocksInSmallRow);
this.setState({
smallRowsAddFlexClass: true,
countBlocksInSmallRow,
smallBlocksRows,
});
}
}
public render() {
this.state.lastConsensusInfo = JSON.stringify(
this.props.appState.consensusInfo
);
const lastBlocks = this.props.appState.consensusInfo;
if (this.props.appState.authorities.length > VIS_AUTHORITIES_LIMIT) {
return (
<div className="Consensus">
<div className="tooManyAuthorities">
<p>Too many authorities.</p>
<p>
Won't display for more than {VIS_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&hellip;
</div>
;
</div>
);
}
let from = 0;
let to = this.state.countBlocksInLargeRow;
const firstLargeRow = this.getLargeRow(lastBlocks.slice(from, to), 0);
from = to;
to = to + this.state.countBlocksInLargeRow;
const secondLargeRow = this.getLargeRow(lastBlocks.slice(from, to), 1);
from = to;
to = to + this.state.smallBlocksRows * this.state.countBlocksInSmallRow;
const smallRow = this.getSmallRow(lastBlocks.slice(from, to));
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) => {
this.setState({ dimensions: contentRect.bounds as BoundingRect });
this.calculateBoxCount(true);
};
private getAuthorities(): Types.Authority[] {
// find the node for each of these authority addresses
if (this.props.appState.authorities == null) {
return [];
}
return this.props.appState.authorities.map((address) => {
const node2 = this.props.appState.nodes
.sorted()
.filter((node) => node.validator === address)[0];
if (!node2) {
return {
Address: address,
NodeId: null,
Name: null,
} as Types.Authority;
}
return {
Address: address,
NodeId: node2.id,
Name: node2.name,
} as Types.Authority;
});
}
private getLargeRow(blocks: Types.ConsensusInfo, id: number) {
const largeBlockSizeChanged = (
isFirstBlock: boolean,
rect: BoundingRect
) => {
if (this.largeBlocksSizeDetected(this.state)) {
return;
}
if (isFirstBlock) {
this.setState({
largeBlockWithLegend: { width: rect.width, height: rect.height },
});
} else {
this.setState({
largeBlock: { width: rect.width, height: rect.height },
});
}
};
const stretchLastRowMajor =
blocks.length < this.state.countBlocksInLargeRow
? 'noStretchOnLastRow'
: '';
const flexClass = this.state.largeRowsAddFlexClass
? 'flexContainerLargeRow'
: '';
return (
<div
className={`ConsensusList LargeRow ${flexClass} ${stretchLastRowMajor}`}
key={`consensusList_${id}`}
>
{blocks.map((item, i) => {
const [height, consensusView] = item;
return (
<ConsensusBlock
changeBlocks={largeBlockSizeChanged}
firstInRow={i === 0}
lastInRow={false}
compact={false}
key={height}
height={height}
measure={!this.state.largeRowsAddFlexClass}
consensusView={consensusView}
authorities={this.getAuthorities()}
authoritySetId={this.props.appState.authoritySetId}
/>
);
})}
</div>
);
}
private getSmallRow(blocks: Types.ConsensusInfo) {
const smallBlockSizeChanged = (
_isFirstBlock: boolean,
rect: BoundingRect
) => {
if (this.smallBlocksSizeDetected(this.state)) {
return;
}
const dimensionsChanged =
this.state.smallBlock.height !== rect.height &&
this.state.smallBlock.width !== rect.width;
if (dimensionsChanged) {
this.setState({
smallBlock: { width: rect.width, height: rect.height },
});
}
};
const stretchLastRow =
blocks.length <
this.state.countBlocksInSmallRow * this.state.smallBlocksRows
? 'noStretchOnLastRow'
: '';
const classes = `ConsensusList SmallRow ${
this.state.smallRowsAddFlexClass ? 'flexContainerSmallRow' : ''
} ${stretchLastRow}`;
return (
<div className={classes} key="smallRow">
{blocks.map((item, i) => {
const [height, consensusView] = item;
let lastInRow =
(i + 1) % this.state.countBlocksInSmallRow === 0 ? true : false;
if (lastInRow && i === 0) {
// should not be marked as last one in row if it's the very first in row
lastInRow = false;
}
return (
<ConsensusBlock
changeBlocks={smallBlockSizeChanged}
firstInRow={i === 0}
lastInRow={lastInRow}
compact={true}
key={height}
height={height}
measure={!this.state.smallRowsAddFlexClass}
consensusView={consensusView}
authorities={this.getAuthorities()}
authoritySetId={this.props.appState.authoritySetId}
/>
);
})}
</div>
);
}
private async subscribeConsensus(chain: Types.GenesisHash) {
const connection = await this.props.connection;
connection.subscribeConsensus(chain);
}
private async unsubscribeConsensus(chain: Types.GenesisHash) {
const connection = await this.props.connection;
connection.unsubscribeConsensus(chain);
}
}
@@ -1,221 +0,0 @@
/*
Source code for the Substrate Telemetry Server.
Copyright (C) 2021 Parity Technologies (UK) Ltd.
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
.Consensus .BlockConsensusMatrice {
background-color: #222;
font-family: monospace, sans-serif;
border-spacing: 0px;
border-right: 2px solid lightgrey;
border-bottom: 1px solid #999;
}
.Consensus .LargeRow .BlockConsensusMatrice:last-child {
border-right: none;
}
.Consensus .SmallRow .lastInRow {
clear: right;
width: 99%;
page-break-after: always;
}
.Consensus .BlockConsensusMatrice th {
font-weight: normal;
border-bottom: 1px dashed #999;
}
.Consensus .finalizedInfo,
.legend {
border-bottom: 1px dotted #555555;
}
.Consensus .finalizedInfo {
white-space: nowrap;
}
.Consensus .finalizedInfo .Tooltip-container {
display: inline-block;
white-space: nowrap;
vertical-align: middle;
}
.Consensus .BlockConsensusMatrice .matrice {
width: 28px;
height: 28px;
}
.Consensus .BlockConsensusMatrice .matrice {
font-weight: normal;
border-right: 1px dotted #555555;
border-bottom: 1px dotted #555;
}
.Consensus .BlockConsensusMatrice tr .matrice:last-child {
border-right: none;
}
.Consensus .BlockConsensusMatrice .matrixXLegend {
text-align: center;
border-right: 1px dotted #555555;
}
.Consensus .BlockConsensusMatrice .matrixXLegend:last-child {
border-right: none;
}
.Consensus .matrice {
text-align: center !important;
min-width: 35px;
}
.Consensus .SmallRow .matrixXLegend,
.Consensus .SmallRow .matrice {
min-width: 26px;
min-height: 26px;
}
.Consensus .finalizedInfo {
text-align: center !important;
}
.Consensus .SmallRow .finalizedInfo {
min-width: 40px;
}
.Consensus .finalizedInfo {
text-align: right;
border-right: 1px dashed #999;
min-width: 50px;
}
.Consensus .finalizedInfo .Tooltip-container {
float: none;
}
.Consensus .explicit {
fill: #e70e81;
}
.Consensus .nodeName {
float: left;
padding-right: 10px;
padding-top: 4px;
}
.Consensus .flexContainerLargeRow .firstInRow .nodeContent {
white-space: nowrap;
}
.Consensus .flexContainerLargeRow .firstInRow .nodeName {
display: inline-block !important;
float: none !important;
vertical-align: middle;
margin-bottom: 3px;
}
.Consensus .flexContainerLargeRow .firstInRow .nodeAddress {
display: inline-block !important;
float: none !important;
vertical-align: middle;
margin-right: 3px;
}
.Consensus .legend {
border-right: 1px solid #999;
white-space: nowrap;
}
.Consensus .first_false .nodeName {
display: none;
}
.Consensus .legend .nodeAddress {
float: right;
}
.Consensus .Row {
color: #999;
cursor: pointer;
}
.Consensus .Row th,
.Consensus .Row td {
text-align: left;
padding: 2px;
}
.Consensus .Row td {
position: relative;
}
.Consensus .Row .Row-truncate {
position: absolute;
left: 0;
right: 0;
top: 0;
padding: inherit;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.Consensus .Row .Row-Tooltip {
position: initial;
padding: inherit;
}
.Consensus .Row:hover {
background-color: #161616;
}
.Consensus .nodeAddress svg {
cursor: pointer;
}
.Consensus .nodeAddress svg:hover {
transform: scale(2);
}
.Consensus .matrice .Icon ~ .Icon {
margin-left: -4px;
}
.Consensus .SmallRow .matrice .Prevote svg {
margin-left: 3px;
margin-bottom: -11px;
}
.Consensus .SmallRow .matrice .Precommit svg {
margin-left: -1px;
margin-top: -6px;
margin-bottom: 0px;
}
.Consensus .jdenticonPlaceholder {
width: 28px;
float: right;
}
.Consensus .SmallRow .jdenticonPlaceholder {
width: 14px;
float: right;
}
.Consensus .even {
background-color: #333;
}
@@ -1,342 +0,0 @@
// Source code for the Substrate Telemetry Server.
// Copyright (C) 2021 Parity Technologies (UK) Ltd.
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with this program. If not, see <https://www.gnu.org/licenses/>.
import * as React from 'react';
import Measure, { BoundingRect, ContentRect } from 'react-measure';
import { Types, Maybe } from '../../common';
import { Icon, Tooltip, PolkadotIcon } from '../';
import Jdenticon from './Jdenticon';
import checkIcon from '../../icons/check.svg';
import finalizedIcon from '../../icons/finalized.svg';
import hatchingIcon from '../../icons/hatching.svg';
import './ConsensusBlock.css';
export namespace ConsensusBlock {
export interface Props {
authorities: Types.Authority[];
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;
let titleFinal = <span>{ratio}</span>;
const majorityFinalized =
finalizedByWhom.length / this.props.authorities.length >= 2 / 3;
if (majorityFinalized && !this.props.compact) {
titleFinal = <span>FINAL</span>;
} else if (majorityFinalized && this.props.compact) {
const hash = this.getFinalizedHash(finalizedByWhom[0]);
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
);
};
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} 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'}
>
&nbsp;
</th>
) : null}
<th
className="legend"
key={'block_row_' + this.props.height + '_legend'}
>
<Tooltip text={`Block number: ${this.props.height}`} />
{this.displayBlockNumber()}
</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>
</table>
</div>
);
};
if (this.props.measure) {
return (
<Measure bounds={true} onResize={handleOnResize}>
{({ measureRef }) => get(measureRef)}
</Measure>
);
} else {
return get(null);
}
}
private displayBlockNumber(): string {
const blockNumber = String(this.props.height);
return blockNumber.length > 2
? '…' + blockNumber.substr(blockNumber.length - 2, blockNumber.length)
: blockNumber;
}
private isFinalized(authority: Types.Authority): boolean {
if (!authority || authority.NodeId == null || authority.Address == null) {
return false;
}
const { Address: addr } = authority;
const consensus = this.props.consensusView;
return (
consensus != null &&
addr in consensus &&
addr in consensus[addr] &&
consensus[addr][addr].Finalized === true
);
}
private getFinalizedHash(authority: Types.Authority): Maybe<Types.BlockHash> {
if (this.isFinalized(authority)) {
const { Address: addr } = authority;
return this.props.consensusView[addr][addr].FinalizedHash;
}
return null;
}
private renderMatriceRow(
authority: Types.Authority,
authorities: Types.Authority[],
row: number
): JSX.Element {
let finalizedInfo = <span>&nbsp;</span>;
let finalizedHash;
if (authority.NodeId != null && this.isFinalized(authority)) {
const matrice = this.props.consensusView[authority.Address][
authority.Address
];
finalizedInfo = matrice.ImplicitFinalized ? (
<Icon className="implicit" src={finalizedIcon} />
) : (
<Icon className="explicit" src={finalizedIcon} />
);
finalizedHash = matrice.FinalizedHash ? (
<Jdenticon hash={matrice.FinalizedHash} size="28px" />
) : (
<div className="jdenticonPlaceholder">&nbsp;</div>
);
}
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" key={'block_row_' + this.props.height + '_' + row}>
{firstName}
<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_' +
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"
key={'authority_' + this.props.height + '_' + authority.Address}
>
<div className="nodeAddress" key={'authority_' + authority.Address}>
<PolkadotIcon
account={authority.Address}
size={this.props.compact ? 14 : 28}
/>
</div>
</div>
);
}
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;
const prevote = consensusInfo && consensusInfo.Prevote;
const implicitPrevote = consensusInfo && consensusInfo.ImplicitPrevote;
const precommit = consensusInfo && consensusInfo.Precommit;
const implicitPrecommit = consensusInfo && consensusInfo.ImplicitPrecommit;
if (rowAuthority.Address !== columnAuthority.Address) {
let statPrevote;
let statPrecommit;
if (implicitPrevote) {
statPrevote = <Icon src={checkIcon} className="implicit" />;
}
if (implicitPrecommit) {
statPrecommit = <Icon src={checkIcon} className="implicit" />;
}
if (prevote) {
statPrevote = <Icon src={checkIcon} className="explicit" />;
}
if (precommit) {
statPrecommit = <Icon src={checkIcon} className="explicit" />;
}
return (
<span key={'icons_pre'}>
{statPrevote}
{statPrecommit}
</span>
);
} else {
return <Icon src={hatchingIcon} className="hatching" />;
}
}
}
@@ -1,27 +0,0 @@
/*
Source code for the Substrate Telemetry Server.
Copyright (C) 2021 Parity Technologies (UK) Ltd.
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
.Jdenticon {
cursor: pointer;
vertical-align: middle;
background-color: #fff;
}
.Jdenticon:hover {
transform: scale(2);
}
@@ -1,61 +0,0 @@
// Source code for the Substrate Telemetry Server.
// Copyright (C) 2021 Parity Technologies (UK) Ltd.
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with this program. If not, see <https://www.gnu.org/licenses/>.
import * as React from 'react';
import './Jdenticon.css';
export interface Props {
hash: string;
size: string;
}
class Jdenticon extends React.Component<Props, {}> {
private element = null;
public componentDidUpdate() {
const jdenticon = (window as any).jdenticon;
if (jdenticon) {
jdenticon.update(this.element);
}
}
public componentDidMount() {
const jdenticon = (window as any).jdenticon;
if (jdenticon) {
jdenticon.update(this.element);
}
}
public render() {
const { hash, size } = this.props;
return (
<svg
className="Jdenticon"
ref={(element) => this.handleRef(element)}
width={size}
height={size}
data-jdenticon-value={hash}
/>
);
}
private handleRef(element: any) {
this.element = element;
}
}
export default Jdenticon;
@@ -1,18 +0,0 @@
// Source code for the Substrate Telemetry Server.
// Copyright (C) 2021 Parity Technologies (UK) Ltd.
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with this program. If not, see <https://www.gnu.org/licenses/>.
export * from './Consensus';
export * from './ConsensusBlock';
-1
View File
@@ -20,7 +20,6 @@ export * from './Chain';
export * from './List';
export * from './Map';
export * from './Settings';
export * from './Consensus';
export * from './Icon';
export * from './Tile';
export * from './Ago';
-5
View File
@@ -268,12 +268,7 @@ export interface State {
status: 'online' | 'offline' | 'upgrade-requested';
best: Types.BlockNumber;
finalized: Types.BlockNumber;
consensusInfo: Types.ConsensusInfo;
displayConsensusLoadingScreen: boolean;
tab: string;
authorities: Types.Address[];
authoritySetId: Maybe<Types.AuthoritySetId>;
sendFinality: boolean;
blockTimestamp: Types.Timestamp;
blockAverage: Maybe<Types.Milliseconds>;
timeDiff: Types.Milliseconds;