123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462 |
- // Copyright 2017-2020 @polkadot/app-explorer authors & contributors
- // This software may be modified and distributed under the terms
- // of the Apache-2.0 license. See the LICENSE file for details.
- import { ApiProps } from '@polkadot/react-api/types';
- import { Header } from '@polkadot/types/interfaces';
- import React, { useCallback, useEffect, useRef, useState } from 'react';
- import styled from 'styled-components';
- import { CardSummary, IdentityIcon, SummaryBox } from '@polkadot/react-components';
- import { useApi } from '@polkadot/react-hooks';
- import { formatNumber } from '@polkadot/util';
- import { useTranslation } from './translate';
- interface LinkHeader {
- author: string | null;
- bn: string;
- hash: string;
- height: number;
- isEmpty: boolean;
- isFinalized: boolean;
- parent: string;
- width: number;
- }
- // eslint-disable-next-line @typescript-eslint/no-empty-interface
- interface LinkArray extends Array<Link> {}
- interface Link {
- arr: LinkArray;
- hdr: LinkHeader;
- }
- interface Props extends ApiProps {
- className?: string;
- finHead?: Header;
- newHead?: Header;
- }
- type UnsubFn = () => void;
- interface Col {
- author: string | null;
- hash: string;
- isEmpty: boolean;
- isFinalized: boolean;
- parent: string;
- width: number;
- }
- interface Row {
- bn: string;
- cols: Col[];
- }
- // adjust the number of columns in a cell based on the children and tree depth
- function calcWidth (children: LinkArray): number {
- return Math.max(1, children.reduce((total, { hdr: { width } }): number => {
- return total + width;
- }, 0));
- }
- // counts the height of a specific node
- function calcHeight (children: LinkArray): number {
- return children.reduce((max, { arr, hdr }): number => {
- hdr.height = hdr.isEmpty
- ? 0
- : 1 + calcHeight(arr);
- return Math.max(max, hdr.height);
- }, 0);
- }
- // a single column in a row, it just has the details for the entry
- function createCol ({ hdr: { author, hash, isEmpty, isFinalized, parent, width } }: Link): Col {
- return { author, hash, isEmpty, isFinalized, parent, width };
- }
- // create a simplified structure that allows for easy rendering
- function createRows (arr: LinkArray): Row[] {
- if (!arr.length) {
- return [];
- }
- return createRows(
- arr.reduce((children: LinkArray, { arr }: Link): LinkArray =>
- children.concat(...arr), [])
- ).concat({
- bn: arr.reduce((result, { hdr: { bn } }): string =>
- result || bn, ''),
- cols: arr.map(createCol)
- });
- }
- // fills in a header based on the supplied data
- function createHdr (bn: string, hash: string, parent: string, author: string | null, isEmpty = false): LinkHeader {
- return { author, bn, hash, height: 0, isEmpty, isFinalized: false, parent, width: 0 };
- }
- // empty link helper
- function createLink (): Link {
- return {
- arr: [],
- hdr: createHdr('', ' ', ' ', null, true)
- };
- }
- // even out the columns, i.e. add empty spacers as applicable to get tree rendering right
- function addColumnSpacers (arr: LinkArray): void {
- // check is any of the children has a non-empty set
- const hasChildren = arr.some(({ arr }): boolean => arr.length !== 0);
- if (hasChildren) {
- // ok, non-empty found - iterate through an add at least an empty cell to all
- arr
- .filter(({ arr }): boolean => arr.length === 0)
- .forEach(({ arr }): number => arr.push(createLink()));
- const newArr = arr.reduce((flat: LinkArray, { arr }): LinkArray => flat.concat(...arr), []);
- // go one level deeper, ensure that the full tree has empty spacers
- addColumnSpacers(newArr);
- }
- }
- // checks to see if a row has a single non-empty entry, i.e. it is a candidate for collapsing
- function isSingleRow (cols: Col[]): boolean {
- if (!cols[0] || cols[0].isEmpty) {
- return false;
- }
- return cols.reduce((result: boolean, col, index): boolean => {
- return index === 0
- ? result
- : (!col.isEmpty ? false : result);
- }, true);
- }
- function renderCol ({ author, hash, isEmpty, isFinalized, parent, width }: Col, index: number): React.ReactNode {
- return (
- <td
- className={`header ${isEmpty ? 'isEmpty' : ''} ${isFinalized ? 'isFinalized' : ''}`}
- colSpan={width}
- key={`${hash}:${index}:${width}`}
- >
- {isEmpty
- ? <div className='empty' />
- : (
- <>
- {author && (
- <IdentityIcon
- className='author'
- size={28}
- value={author}
- />
- )}
- <div className='contents'>
- <div className='hash'>{hash}</div>
- <div className='parent'>{parent}</div>
- </div>
- </>
- )
- }
- </td>
- );
- }
- // render the rows created by createRows to React nodes
- function renderRows (rows: Row[]): React.ReactNode[] {
- const lastIndex = rows.length - 1;
- let isPrevShort = false;
- return rows.map(({ bn, cols }, index): React.ReactNode => {
- // if not first, not last and single only, see if we can collapse
- if (index !== 0 && index !== lastIndex && isSingleRow(cols)) {
- if (isPrevShort) {
- // previous one was already a link, this one as well - skip it
- return null;
- } else if (isSingleRow(rows[index - 1].cols)) {
- isPrevShort = true;
- return (
- <tr key={bn}>
- <td key='blockNumber' />
- <td
- className='header isLink'
- colSpan={cols[0].width}
- >
- <div className='link'>⋮</div>
- </td>
- </tr>
- );
- }
- }
- isPrevShort = false;
- return (
- <tr key={bn}>
- <td key='blockNumber'>{`#${bn}`}</td>
- {cols.map(renderCol)}
- </tr>
- );
- });
- }
- function Forks ({ className }: Props): React.ReactElement<Props> | null {
- const { t } = useTranslation();
- const { api } = useApi();
- const [tree, setTree] = useState<Link | null>(null);
- const childrenRef = useRef<Map<string, string[]>>(new Map([['root', []]]));
- const countRef = useRef({ numBlocks: 0, numForks: 0 });
- const headersRef = useRef<Map<string, LinkHeader>>(new Map());
- const firstNumRef = useRef('');
- const _finalize = useCallback(
- (hash: string): void => {
- const hdr = headersRef.current.get(hash);
- if (hdr && !hdr.isFinalized) {
- hdr.isFinalized = true;
- _finalize(hdr.parent);
- }
- },
- []
- );
- // adds children for a specific header, retrieving based on matching parent
- const _addChildren = useCallback(
- (base: LinkHeader, children: LinkArray): LinkArray => {
- // add the children
- (childrenRef.current.get(base.hash) || [])
- .map((hash): LinkHeader | undefined => headersRef.current.get(hash))
- .filter((hdr): hdr is LinkHeader => !!hdr)
- .forEach((hdr): void => {
- children.push({ arr: _addChildren(hdr, []), hdr });
- });
- // calculate the max height/width for this entry
- base.height = calcHeight(children);
- base.width = calcWidth(children);
- // place the active (larger, finalized) columns first for the pyramid display
- children.sort((a, b): number =>
- (a.hdr.width > b.hdr.width || a.hdr.height > b.hdr.height || a.hdr.isFinalized)
- ? -1
- : (a.hdr.width < b.hdr.width || a.hdr.height < b.hdr.height || b.hdr.isFinalized)
- ? 1
- : 0
- );
- return children;
- },
- []
- );
- // create a tree list from the available headers
- const _generateTree = useCallback(
- (): Link => {
- const root = createLink();
- // add all the root entries first, we iterate from these
- // We add the root entry explicitly, it exists as per init
- (childrenRef.current.get('root') || []).forEach((hash): void => {
- const hdr = headersRef.current.get(hash);
- // if this fails, well, we have a bigger issue :(
- if (hdr) {
- root.arr.push({ arr: [], hdr: { ...hdr } });
- }
- });
- // iterate through, adding the children for each of the root nodes
- root.arr.forEach(({ arr, hdr }): void => {
- _addChildren(hdr, arr);
- });
- // align the columns with empty spacers - this aids in display
- addColumnSpacers(root.arr);
- root.hdr.height = calcHeight(root.arr);
- root.hdr.width = calcWidth(root.arr);
- return root;
- },
- [_addChildren]
- );
- // callback when finalized
- const _newFinalized = useCallback(
- (header: Header): void => {
- _finalize(header.hash.toHex());
- },
- [_finalize]
- );
- // callback for the subscribe headers sub
- const _newHeader = useCallback(
- (header: Header): void => {
- // formatted block info
- const bn = formatNumber(header.number);
- const hash = header.hash.toHex();
- const parent = header.parentHash.toHex();
- let isFork = false;
- // if this the first one?
- if (!firstNumRef.current) {
- firstNumRef.current = bn;
- }
- if (!headersRef.current.has(hash)) {
- // if this is the first, add to the root entry
- if (firstNumRef.current === bn) {
- (childrenRef.current.get('root') as any[]).push(hash);
- }
- // add to the header map
- // also for HeaderExtended header.author ? header.author.toString() : null
- headersRef.current.set(hash, createHdr(bn, hash, parent, null));
- // check to see if the children already has a entry
- if (childrenRef.current.has(parent)) {
- isFork = true;
- (childrenRef.current.get(parent) as any[]).push(hash);
- } else {
- childrenRef.current.set(parent, [hash]);
- }
- // if we don't have the parent of this one, retrieve it
- if (!headersRef.current.has(parent)) {
- // just make sure we are not first in the list, we don't want to full chain
- if (firstNumRef.current !== bn) {
- console.warn(`Retrieving missing header ${header.parentHash.toHex()}`);
- api.rpc.chain.getHeader(header.parentHash).then(_newHeader).catch(console.error);
- // catch the refresh on the result
- return;
- }
- }
- // update our counters
- countRef.current.numBlocks++;
- if (isFork) {
- countRef.current.numForks++;
- }
- // do the magic, extract the info into something useful and add to state
- setTree(_generateTree());
- }
- },
- [api, _generateTree]
- );
- useEffect((): () => void => {
- let _subFinHead: UnsubFn | null = null;
- let _subNewHead: UnsubFn | null = null;
- (async (): Promise<void> => {
- _subFinHead = await api.rpc.chain.subscribeFinalizedHeads(_newFinalized);
- _subNewHead = await api.rpc.chain.subscribeNewHeads(_newHeader);
- })().catch(console.error);
- return (): void => {
- _subFinHead && _subFinHead();
- _subNewHead && _subNewHead();
- };
- }, [api, _newFinalized, _newHeader]);
- if (!tree) {
- return null;
- }
- return (
- <div className={className}>
- <SummaryBox>
- <section>
- <CardSummary label={t<string>('blocks')}>{formatNumber(countRef.current.numBlocks)}</CardSummary>
- <CardSummary label={t<string>('forks')}>{formatNumber(countRef.current.numForks)}</CardSummary>
- </section>
- </SummaryBox>
- <table>
- <tbody>
- {renderRows(createRows(tree.arr))}
- </tbody>
- </table>
- </div>
- );
- }
- export default React.memo(styled(Forks)`
- margin-bottom: 1.5rem;
- table {
- border-collapse: separate;
- border-spacing: 0.25rem;
- font-family: monospace;
- td {
- padding: 0.25rem 0.5rem;
- text-align: center;
- .author,
- .contents {
- display: inline-block;
- vertical-align: middle;
- }
- .author {
- margin-right: 0.25rem;
- }
- .contents {
- .hash, .parent {
- margin: 0 auto;
- max-width: 6rem;
- overflow: hidden;
- text-overflow: ellipsis;
- white-space: nowrap;
- }
- .parent {
- font-size: 0.75rem;
- line-height: 0.75rem;
- max-width: 4.5rem;
- }
- }
- &.blockNumber {
- font-size: 1.25rem;
- }
- &.header {
- background: #fff;
- border: 1px solid #e6e6e6;
- border-radius: 0.25rem;
- &.isEmpty {
- background: transparent;
- border-color: transparent;
- }
- &.isFinalized {
- background: rgba(0, 255, 0, 0.1);
- }
- &.isLink {
- background: transparent;
- border-color: transparent;
- line-height: 1rem;
- padding: 0;
- }
- &.isMissing {
- background: rgba(255, 0, 0, 0.05);
- }
- }
- }
- }
- `);
|