Forks.tsx 12 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462
  1. // Copyright 2017-2020 @polkadot/app-explorer authors & contributors
  2. // This software may be modified and distributed under the terms
  3. // of the Apache-2.0 license. See the LICENSE file for details.
  4. import { ApiProps } from '@polkadot/react-api/types';
  5. import { Header } from '@polkadot/types/interfaces';
  6. import React, { useCallback, useEffect, useRef, useState } from 'react';
  7. import styled from 'styled-components';
  8. import { CardSummary, IdentityIcon, SummaryBox } from '@polkadot/react-components';
  9. import { useApi } from '@polkadot/react-hooks';
  10. import { formatNumber } from '@polkadot/util';
  11. import { useTranslation } from './translate';
  12. interface LinkHeader {
  13. author: string | null;
  14. bn: string;
  15. hash: string;
  16. height: number;
  17. isEmpty: boolean;
  18. isFinalized: boolean;
  19. parent: string;
  20. width: number;
  21. }
  22. // eslint-disable-next-line @typescript-eslint/no-empty-interface
  23. interface LinkArray extends Array<Link> {}
  24. interface Link {
  25. arr: LinkArray;
  26. hdr: LinkHeader;
  27. }
  28. interface Props extends ApiProps {
  29. className?: string;
  30. finHead?: Header;
  31. newHead?: Header;
  32. }
  33. type UnsubFn = () => void;
  34. interface Col {
  35. author: string | null;
  36. hash: string;
  37. isEmpty: boolean;
  38. isFinalized: boolean;
  39. parent: string;
  40. width: number;
  41. }
  42. interface Row {
  43. bn: string;
  44. cols: Col[];
  45. }
  46. // adjust the number of columns in a cell based on the children and tree depth
  47. function calcWidth (children: LinkArray): number {
  48. return Math.max(1, children.reduce((total, { hdr: { width } }): number => {
  49. return total + width;
  50. }, 0));
  51. }
  52. // counts the height of a specific node
  53. function calcHeight (children: LinkArray): number {
  54. return children.reduce((max, { arr, hdr }): number => {
  55. hdr.height = hdr.isEmpty
  56. ? 0
  57. : 1 + calcHeight(arr);
  58. return Math.max(max, hdr.height);
  59. }, 0);
  60. }
  61. // a single column in a row, it just has the details for the entry
  62. function createCol ({ hdr: { author, hash, isEmpty, isFinalized, parent, width } }: Link): Col {
  63. return { author, hash, isEmpty, isFinalized, parent, width };
  64. }
  65. // create a simplified structure that allows for easy rendering
  66. function createRows (arr: LinkArray): Row[] {
  67. if (!arr.length) {
  68. return [];
  69. }
  70. return createRows(
  71. arr.reduce((children: LinkArray, { arr }: Link): LinkArray =>
  72. children.concat(...arr), [])
  73. ).concat({
  74. bn: arr.reduce((result, { hdr: { bn } }): string =>
  75. result || bn, ''),
  76. cols: arr.map(createCol)
  77. });
  78. }
  79. // fills in a header based on the supplied data
  80. function createHdr (bn: string, hash: string, parent: string, author: string | null, isEmpty = false): LinkHeader {
  81. return { author, bn, hash, height: 0, isEmpty, isFinalized: false, parent, width: 0 };
  82. }
  83. // empty link helper
  84. function createLink (): Link {
  85. return {
  86. arr: [],
  87. hdr: createHdr('', ' ', ' ', null, true)
  88. };
  89. }
  90. // even out the columns, i.e. add empty spacers as applicable to get tree rendering right
  91. function addColumnSpacers (arr: LinkArray): void {
  92. // check is any of the children has a non-empty set
  93. const hasChildren = arr.some(({ arr }): boolean => arr.length !== 0);
  94. if (hasChildren) {
  95. // ok, non-empty found - iterate through an add at least an empty cell to all
  96. arr
  97. .filter(({ arr }): boolean => arr.length === 0)
  98. .forEach(({ arr }): number => arr.push(createLink()));
  99. const newArr = arr.reduce((flat: LinkArray, { arr }): LinkArray => flat.concat(...arr), []);
  100. // go one level deeper, ensure that the full tree has empty spacers
  101. addColumnSpacers(newArr);
  102. }
  103. }
  104. // checks to see if a row has a single non-empty entry, i.e. it is a candidate for collapsing
  105. function isSingleRow (cols: Col[]): boolean {
  106. if (!cols[0] || cols[0].isEmpty) {
  107. return false;
  108. }
  109. return cols.reduce((result: boolean, col, index): boolean => {
  110. return index === 0
  111. ? result
  112. : (!col.isEmpty ? false : result);
  113. }, true);
  114. }
  115. function renderCol ({ author, hash, isEmpty, isFinalized, parent, width }: Col, index: number): React.ReactNode {
  116. return (
  117. <td
  118. className={`header ${isEmpty ? 'isEmpty' : ''} ${isFinalized ? 'isFinalized' : ''}`}
  119. colSpan={width}
  120. key={`${hash}:${index}:${width}`}
  121. >
  122. {isEmpty
  123. ? <div className='empty' />
  124. : (
  125. <>
  126. {author && (
  127. <IdentityIcon
  128. className='author'
  129. size={28}
  130. value={author}
  131. />
  132. )}
  133. <div className='contents'>
  134. <div className='hash'>{hash}</div>
  135. <div className='parent'>{parent}</div>
  136. </div>
  137. </>
  138. )
  139. }
  140. </td>
  141. );
  142. }
  143. // render the rows created by createRows to React nodes
  144. function renderRows (rows: Row[]): React.ReactNode[] {
  145. const lastIndex = rows.length - 1;
  146. let isPrevShort = false;
  147. return rows.map(({ bn, cols }, index): React.ReactNode => {
  148. // if not first, not last and single only, see if we can collapse
  149. if (index !== 0 && index !== lastIndex && isSingleRow(cols)) {
  150. if (isPrevShort) {
  151. // previous one was already a link, this one as well - skip it
  152. return null;
  153. } else if (isSingleRow(rows[index - 1].cols)) {
  154. isPrevShort = true;
  155. return (
  156. <tr key={bn}>
  157. <td key='blockNumber' />
  158. <td
  159. className='header isLink'
  160. colSpan={cols[0].width}
  161. >
  162. <div className='link'>&#8942;</div>
  163. </td>
  164. </tr>
  165. );
  166. }
  167. }
  168. isPrevShort = false;
  169. return (
  170. <tr key={bn}>
  171. <td key='blockNumber'>{`#${bn}`}</td>
  172. {cols.map(renderCol)}
  173. </tr>
  174. );
  175. });
  176. }
  177. function Forks ({ className }: Props): React.ReactElement<Props> | null {
  178. const { t } = useTranslation();
  179. const { api } = useApi();
  180. const [tree, setTree] = useState<Link | null>(null);
  181. const childrenRef = useRef<Map<string, string[]>>(new Map([['root', []]]));
  182. const countRef = useRef({ numBlocks: 0, numForks: 0 });
  183. const headersRef = useRef<Map<string, LinkHeader>>(new Map());
  184. const firstNumRef = useRef('');
  185. const _finalize = useCallback(
  186. (hash: string): void => {
  187. const hdr = headersRef.current.get(hash);
  188. if (hdr && !hdr.isFinalized) {
  189. hdr.isFinalized = true;
  190. _finalize(hdr.parent);
  191. }
  192. },
  193. []
  194. );
  195. // adds children for a specific header, retrieving based on matching parent
  196. const _addChildren = useCallback(
  197. (base: LinkHeader, children: LinkArray): LinkArray => {
  198. // add the children
  199. (childrenRef.current.get(base.hash) || [])
  200. .map((hash): LinkHeader | undefined => headersRef.current.get(hash))
  201. .filter((hdr): hdr is LinkHeader => !!hdr)
  202. .forEach((hdr): void => {
  203. children.push({ arr: _addChildren(hdr, []), hdr });
  204. });
  205. // calculate the max height/width for this entry
  206. base.height = calcHeight(children);
  207. base.width = calcWidth(children);
  208. // place the active (larger, finalized) columns first for the pyramid display
  209. children.sort((a, b): number =>
  210. (a.hdr.width > b.hdr.width || a.hdr.height > b.hdr.height || a.hdr.isFinalized)
  211. ? -1
  212. : (a.hdr.width < b.hdr.width || a.hdr.height < b.hdr.height || b.hdr.isFinalized)
  213. ? 1
  214. : 0
  215. );
  216. return children;
  217. },
  218. []
  219. );
  220. // create a tree list from the available headers
  221. const _generateTree = useCallback(
  222. (): Link => {
  223. const root = createLink();
  224. // add all the root entries first, we iterate from these
  225. // We add the root entry explicitly, it exists as per init
  226. (childrenRef.current.get('root') || []).forEach((hash): void => {
  227. const hdr = headersRef.current.get(hash);
  228. // if this fails, well, we have a bigger issue :(
  229. if (hdr) {
  230. root.arr.push({ arr: [], hdr: { ...hdr } });
  231. }
  232. });
  233. // iterate through, adding the children for each of the root nodes
  234. root.arr.forEach(({ arr, hdr }): void => {
  235. _addChildren(hdr, arr);
  236. });
  237. // align the columns with empty spacers - this aids in display
  238. addColumnSpacers(root.arr);
  239. root.hdr.height = calcHeight(root.arr);
  240. root.hdr.width = calcWidth(root.arr);
  241. return root;
  242. },
  243. [_addChildren]
  244. );
  245. // callback when finalized
  246. const _newFinalized = useCallback(
  247. (header: Header): void => {
  248. _finalize(header.hash.toHex());
  249. },
  250. [_finalize]
  251. );
  252. // callback for the subscribe headers sub
  253. const _newHeader = useCallback(
  254. (header: Header): void => {
  255. // formatted block info
  256. const bn = formatNumber(header.number);
  257. const hash = header.hash.toHex();
  258. const parent = header.parentHash.toHex();
  259. let isFork = false;
  260. // if this the first one?
  261. if (!firstNumRef.current) {
  262. firstNumRef.current = bn;
  263. }
  264. if (!headersRef.current.has(hash)) {
  265. // if this is the first, add to the root entry
  266. if (firstNumRef.current === bn) {
  267. (childrenRef.current.get('root') as any[]).push(hash);
  268. }
  269. // add to the header map
  270. // also for HeaderExtended header.author ? header.author.toString() : null
  271. headersRef.current.set(hash, createHdr(bn, hash, parent, null));
  272. // check to see if the children already has a entry
  273. if (childrenRef.current.has(parent)) {
  274. isFork = true;
  275. (childrenRef.current.get(parent) as any[]).push(hash);
  276. } else {
  277. childrenRef.current.set(parent, [hash]);
  278. }
  279. // if we don't have the parent of this one, retrieve it
  280. if (!headersRef.current.has(parent)) {
  281. // just make sure we are not first in the list, we don't want to full chain
  282. if (firstNumRef.current !== bn) {
  283. console.warn(`Retrieving missing header ${header.parentHash.toHex()}`);
  284. api.rpc.chain.getHeader(header.parentHash).then(_newHeader).catch(console.error);
  285. // catch the refresh on the result
  286. return;
  287. }
  288. }
  289. // update our counters
  290. countRef.current.numBlocks++;
  291. if (isFork) {
  292. countRef.current.numForks++;
  293. }
  294. // do the magic, extract the info into something useful and add to state
  295. setTree(_generateTree());
  296. }
  297. },
  298. [api, _generateTree]
  299. );
  300. useEffect((): () => void => {
  301. let _subFinHead: UnsubFn | null = null;
  302. let _subNewHead: UnsubFn | null = null;
  303. (async (): Promise<void> => {
  304. _subFinHead = await api.rpc.chain.subscribeFinalizedHeads(_newFinalized);
  305. _subNewHead = await api.rpc.chain.subscribeNewHeads(_newHeader);
  306. })().catch(console.error);
  307. return (): void => {
  308. _subFinHead && _subFinHead();
  309. _subNewHead && _subNewHead();
  310. };
  311. }, [api, _newFinalized, _newHeader]);
  312. if (!tree) {
  313. return null;
  314. }
  315. return (
  316. <div className={className}>
  317. <SummaryBox>
  318. <section>
  319. <CardSummary label={t<string>('blocks')}>{formatNumber(countRef.current.numBlocks)}</CardSummary>
  320. <CardSummary label={t<string>('forks')}>{formatNumber(countRef.current.numForks)}</CardSummary>
  321. </section>
  322. </SummaryBox>
  323. <table>
  324. <tbody>
  325. {renderRows(createRows(tree.arr))}
  326. </tbody>
  327. </table>
  328. </div>
  329. );
  330. }
  331. export default React.memo(styled(Forks)`
  332. margin-bottom: 1.5rem;
  333. table {
  334. border-collapse: separate;
  335. border-spacing: 0.25rem;
  336. font-family: monospace;
  337. td {
  338. padding: 0.25rem 0.5rem;
  339. text-align: center;
  340. .author,
  341. .contents {
  342. display: inline-block;
  343. vertical-align: middle;
  344. }
  345. .author {
  346. margin-right: 0.25rem;
  347. }
  348. .contents {
  349. .hash, .parent {
  350. margin: 0 auto;
  351. max-width: 6rem;
  352. overflow: hidden;
  353. text-overflow: ellipsis;
  354. white-space: nowrap;
  355. }
  356. .parent {
  357. font-size: 0.75rem;
  358. line-height: 0.75rem;
  359. max-width: 4.5rem;
  360. }
  361. }
  362. &.blockNumber {
  363. font-size: 1.25rem;
  364. }
  365. &.header {
  366. background: #fff;
  367. border: 1px solid #e6e6e6;
  368. border-radius: 0.25rem;
  369. &.isEmpty {
  370. background: transparent;
  371. border-color: transparent;
  372. }
  373. &.isFinalized {
  374. background: rgba(0, 255, 0, 0.1);
  375. }
  376. &.isLink {
  377. background: transparent;
  378. border-color: transparent;
  379. line-height: 1rem;
  380. padding: 0;
  381. }
  382. &.isMissing {
  383. background: rgba(255, 0, 0, 0.05);
  384. }
  385. }
  386. }
  387. }
  388. `);