mirror of
https://github.com/pezkuwichain/pezkuwi-telemetry.git
synced 2026-06-15 21:11:02 +00:00
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:
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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,
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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'>;
|
||||
|
||||
@@ -70,6 +70,5 @@ export class AllChains extends React.Component<AllChains.Props, {}> {
|
||||
const connection = await this.props.connection;
|
||||
|
||||
connection.subscribe(chain);
|
||||
connection.resetConsensus();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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…
|
||||
</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'}
|
||||
>
|
||||
|
||||
</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> </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"> </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';
|
||||
@@ -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';
|
||||
|
||||
@@ -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;
|
||||
|
||||
Reference in New Issue
Block a user