initial commit for public repo
Signed-off-by: Uncle Fatso <uncle.fatso@ghostchain.io>
							
								
								
									
										1
									
								
								.env.template
									
									
									
									
									
										Normal file
									
								
							
							
						
						@ -0,0 +1 @@
 | 
				
			|||||||
 | 
					REACT_APP_TRACKING_ID=
 | 
				
			||||||
							
								
								
									
										40
									
								
								.eslintrc.json
									
									
									
									
									
										Normal file
									
								
							
							
						
						@ -0,0 +1,40 @@
 | 
				
			|||||||
 | 
					{
 | 
				
			||||||
 | 
					  "env": {
 | 
				
			||||||
 | 
					    "browser": true,
 | 
				
			||||||
 | 
					    "es2021": true
 | 
				
			||||||
 | 
					  },
 | 
				
			||||||
 | 
					  "extends": [
 | 
				
			||||||
 | 
					    "eslint:recommended",
 | 
				
			||||||
 | 
					    "plugin:@typescript-eslint/recommended",
 | 
				
			||||||
 | 
					    "plugin:react/jsx-runtime",
 | 
				
			||||||
 | 
					    "plugin:react/recommended"
 | 
				
			||||||
 | 
					  ],
 | 
				
			||||||
 | 
					  "parser": "@typescript-eslint/parser",
 | 
				
			||||||
 | 
					  "parserOptions": {
 | 
				
			||||||
 | 
					    "ecmaFeatures": {
 | 
				
			||||||
 | 
					      "jsx": true
 | 
				
			||||||
 | 
					    },
 | 
				
			||||||
 | 
					    "ecmaVersion": 12,
 | 
				
			||||||
 | 
					    "sourceType": "module"
 | 
				
			||||||
 | 
					  },
 | 
				
			||||||
 | 
					  "plugins": ["react", "react-hooks", "@typescript-eslint"],
 | 
				
			||||||
 | 
					  "rules": {
 | 
				
			||||||
 | 
					    "no-console": 0,
 | 
				
			||||||
 | 
					    "no-empty": 0,
 | 
				
			||||||
 | 
					    "no-bitwise": 0,
 | 
				
			||||||
 | 
					    "quotes": [1, "single"],
 | 
				
			||||||
 | 
					    "jsx-quotes": [1, "prefer-double"],
 | 
				
			||||||
 | 
					    "semi": [1, "always"],
 | 
				
			||||||
 | 
					    "@typescript-eslint/prefer-as-const": 1,
 | 
				
			||||||
 | 
					    "@typescript-eslint/no-namespace": 1,
 | 
				
			||||||
 | 
					    "no-unused-vars": "off",
 | 
				
			||||||
 | 
					    "@typescript-eslint/no-unused-vars": [
 | 
				
			||||||
 | 
					      "warn",
 | 
				
			||||||
 | 
					      {
 | 
				
			||||||
 | 
					        "argsIgnorePattern": "^_",
 | 
				
			||||||
 | 
					        "varsIgnorePattern": "^_",
 | 
				
			||||||
 | 
					        "caughtErrorsIgnorePattern": "^_"
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					    ]
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
							
								
								
									
										5
									
								
								.gitignore
									
									
									
									
										vendored
									
									
										Normal file
									
								
							
							
						
						@ -0,0 +1,5 @@
 | 
				
			|||||||
 | 
					.env
 | 
				
			||||||
 | 
					.nyc
 | 
				
			||||||
 | 
					.nyc_output
 | 
				
			||||||
 | 
					node_modules/
 | 
				
			||||||
 | 
					build/
 | 
				
			||||||
							
								
								
									
										4
									
								
								.prettierrc
									
									
									
									
									
										Normal file
									
								
							
							
						
						@ -0,0 +1,4 @@
 | 
				
			|||||||
 | 
					{
 | 
				
			||||||
 | 
					  "singleQuote": true,
 | 
				
			||||||
 | 
					  "semi": true
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
							
								
								
									
										5
									
								
								assets/favicon.svg
									
									
									
									
									
										Normal file
									
								
							
							
						
						@ -0,0 +1,5 @@
 | 
				
			|||||||
 | 
					<svg width="278" height="278" viewBox="0 0 250 278" fill="none" xmlns="http://www.w3.org/2000/svg">
 | 
				
			||||||
 | 
					<path d="M122.222 127.504L23.3996 71.0796C18.5633 68.3182 18.5334 61.3555 23.346 58.5528L122.199 0.982327C124.442 -0.3236 127.212 -0.32769 129.458 0.971606L228.12 58.0407C232.937 60.8269 232.933 67.7821 228.112 70.5624L129.42 127.488C127.194 128.772 124.454 128.778 122.222 127.504Z" fill="#E6007A"/>
 | 
				
			||||||
 | 
					<path d="M246.368 219.626L148.092 276.997C143.282 279.805 137.237 276.349 137.216 270.78L136.785 156.385C136.776 153.791 138.157 151.389 140.406 150.094L239.16 93.1846C243.981 90.4063 250.002 93.8876 250 99.4522L249.953 213.385C249.952 215.955 248.587 218.331 246.368 219.626Z" fill="#E6007A"/>
 | 
				
			||||||
 | 
					<path d="M113.198 156.338L113.745 270.133C113.771 275.702 107.756 279.21 102.923 276.443L3.63874 219.619C1.38658 218.33 -0.00209701 215.933 2.37701e-06 213.338L0.0923778 99.3597C0.0968919 93.7951 6.12245 90.3212 10.9404 93.1055L109.586 150.113C111.81 151.399 113.186 153.769 113.198 156.338Z" fill="#E6007A"/>
 | 
				
			||||||
 | 
					</svg>
 | 
				
			||||||
| 
		 After Width: | Height: | Size: 1013 B  | 
							
								
								
									
										46
									
								
								assets/index.html
									
									
									
									
									
										Normal file
									
								
							
							
						
						@ -0,0 +1,46 @@
 | 
				
			|||||||
 | 
					<!DOCTYPE html>
 | 
				
			||||||
 | 
					<html lang="en">
 | 
				
			||||||
 | 
					  <head>
 | 
				
			||||||
 | 
					    <meta charset="utf-8" />
 | 
				
			||||||
 | 
					    <meta
 | 
				
			||||||
 | 
					      name="viewport"
 | 
				
			||||||
 | 
					      content="width=device-width, initial-scale=1, shrink-to-fit=no"
 | 
				
			||||||
 | 
					    />
 | 
				
			||||||
 | 
					    <meta name="theme-color" content="#000000" />
 | 
				
			||||||
 | 
					    <title>Ghost Telemetry</title>
 | 
				
			||||||
 | 
					    <meta name="description" content="The GHOST telemetry dashboard provides a real-time view of how currently online nodes are performing." />
 | 
				
			||||||
 | 
					    <meta name="keywords" content="ghostchain, crypto, blockchain, anonymity, privacy, freedom" />
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    <style>
 | 
				
			||||||
 | 
					      html {
 | 
				
			||||||
 | 
					        background: linear-gradient(90deg, rgba(28,54,100,1) 0%, rgba(45,87,132,1) 100%);
 | 
				
			||||||
 | 
					        color: #f2e370;
 | 
				
			||||||
 | 
					        background-repeat: no-repeat;
 | 
				
			||||||
 | 
					        height: 100%;
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					    </style>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    <meta property="og:image" content="https://blog.ghostchain.io/wp-content/uploads/2024/12/GHOST-Telemetry-Featured_Images.png" />
 | 
				
			||||||
 | 
					    <meta property="og:title" content="Ghost Telemetry" />
 | 
				
			||||||
 | 
					    <meta property="og:image:type" content="image/png" />
 | 
				
			||||||
 | 
					    <meta property="og:image:width" content="1200" />
 | 
				
			||||||
 | 
					    <meta property="og:image:height" content="630" />
 | 
				
			||||||
 | 
					    <meta property="og:type" content="website" />
 | 
				
			||||||
 | 
					    <meta property="og:description" content="The GHOST telemetry dashboard provides a real-time view of how currently online nodes are performing." />
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    <meta name="twitter:card" content="summary" />
 | 
				
			||||||
 | 
					    <meta name="twitter:site" content="@realGhostChain" />
 | 
				
			||||||
 | 
					    <meta name="twitter:title" content="Ghost Telemetry" />
 | 
				
			||||||
 | 
					    <meta name="twitter:description" content="The GHOST telemetry dashboard provides a real-time view of how currently online nodes are performing." />
 | 
				
			||||||
 | 
					    <meta name="twitter:image" content="https://blog.ghostchain.io/wp-content/uploads/2024/12/GHOST-Telemetry-Featured_Images.png" />
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    <link rel="icon" href="https://ghostchain.io/wp-content/uploads/2024/10/cropped-logo-32x32.png" sizes="32x32" />
 | 
				
			||||||
 | 
					    <link rel="icon" href="https://ghostchain.io/wp-content/uploads/2024/10/cropped-logo-192x192.png" sizes="192x192" />
 | 
				
			||||||
 | 
					    <link rel="apple-touch-icon" href="https://ghostchain.io/wp-content/uploads/2024/10/cropped-logo-180x180.png" />
 | 
				
			||||||
 | 
					    <link href="https://fonts.googleapis.com/css2?family=Ubuntu:wght@400;700&display=swap" rel="stylesheet">
 | 
				
			||||||
 | 
					  </head>
 | 
				
			||||||
 | 
					  <body>
 | 
				
			||||||
 | 
					    <noscript> You need to enable JavaScript to run this app. </noscript>
 | 
				
			||||||
 | 
					    <div id="root"></div>
 | 
				
			||||||
 | 
					  </body>
 | 
				
			||||||
 | 
					</html>
 | 
				
			||||||
							
								
								
									
										
											BIN
										
									
								
								assets/meta-image.png
									
									
									
									
									
										Normal file
									
								
							
							
						
						| 
		 After Width: | Height: | Size: 217 KiB  | 
							
								
								
									
										3
									
								
								assets/mock.image.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						@ -0,0 +1,3 @@
 | 
				
			|||||||
 | 
					// loading images gives back a path. This is what we replace
 | 
				
			||||||
 | 
					// that with for tests:
 | 
				
			||||||
 | 
					module.exports = 'test-image-stub';
 | 
				
			||||||
							
								
								
									
										2
									
								
								assets/mock.style.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						@ -0,0 +1,2 @@
 | 
				
			|||||||
 | 
					// For loading styles, give back an empty object in tests:
 | 
				
			||||||
 | 
					module.exports = {};
 | 
				
			||||||
							
								
								
									
										3
									
								
								images.d.ts
									
									
									
									
										vendored
									
									
										Normal file
									
								
							
							
						
						@ -0,0 +1,3 @@
 | 
				
			|||||||
 | 
					declare module '*.svg';
 | 
				
			||||||
 | 
					declare module '*.png';
 | 
				
			||||||
 | 
					declare module '*.jpg';
 | 
				
			||||||
							
								
								
									
										11
									
								
								jest.config.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						@ -0,0 +1,11 @@
 | 
				
			|||||||
 | 
					/** @type {import('ts-jest').JestConfigWithTsJest} */
 | 
				
			||||||
 | 
					module.exports = {
 | 
				
			||||||
 | 
					  preset: 'ts-jest',
 | 
				
			||||||
 | 
					  testEnvironment: 'jsdom',
 | 
				
			||||||
 | 
					  setupFiles: ['<rootDir>/setupJest.js'],
 | 
				
			||||||
 | 
					  moduleNameMapper: {
 | 
				
			||||||
 | 
					    '\\.(jpg|jpeg|png|gif|eot|otf|webp|svg|ttf|woff|woff2|mp4|webm|wav|mp3|m4a|aac|oga)$':
 | 
				
			||||||
 | 
					      '<rootDir>/assets/mock.image.js',
 | 
				
			||||||
 | 
					    '\\.(css|less|scss|sass)$': '<rootDir>/assets/mock.style.js',
 | 
				
			||||||
 | 
					  },
 | 
				
			||||||
 | 
					};
 | 
				
			||||||
							
								
								
									
										16809
									
								
								package-lock.json
									
									
									
										generated
									
									
									
										Normal file
									
								
							
							
						
						
							
								
								
									
										69
									
								
								package.json
									
									
									
									
									
										Normal file
									
								
							
							
						
						@ -0,0 +1,69 @@
 | 
				
			|||||||
 | 
					{
 | 
				
			||||||
 | 
					  "name": "ghost-telemetry-frontend",
 | 
				
			||||||
 | 
					  "version": "0.1.5",
 | 
				
			||||||
 | 
					  "author": "Uncle f4tso <f4ts0@ghostchain.io>",
 | 
				
			||||||
 | 
					  "license": "GPL-3.0",
 | 
				
			||||||
 | 
					  "description": "Ghost Telemetry frontend",
 | 
				
			||||||
 | 
					  "scripts": {
 | 
				
			||||||
 | 
					    "start": "webpack serve --mode=development",
 | 
				
			||||||
 | 
					    "build": "webpack --mode=production",
 | 
				
			||||||
 | 
					    "test": "jest",
 | 
				
			||||||
 | 
					    "pretty:check": "prettier --check src/**/*.{ts,tsx}",
 | 
				
			||||||
 | 
					    "pretty:fix": "prettier --write src",
 | 
				
			||||||
 | 
					    "clean": "rm -rf node_modules build ./tmp/env-config.js report*.json npm-error.log",
 | 
				
			||||||
 | 
					    "lint": "eslint src"
 | 
				
			||||||
 | 
					  },
 | 
				
			||||||
 | 
					  "dependencies": {
 | 
				
			||||||
 | 
					    "@polkadot/util-crypto": "^10.1.7",
 | 
				
			||||||
 | 
					    "dotenv-webpack": "^8.1.0",
 | 
				
			||||||
 | 
					    "react": "^18.2.0",
 | 
				
			||||||
 | 
					    "react-dom": "^18.2.0",
 | 
				
			||||||
 | 
					    "react-ga4": "^2.1.0"
 | 
				
			||||||
 | 
					  },
 | 
				
			||||||
 | 
					  "devDependencies": {
 | 
				
			||||||
 | 
					    "@types/jest": "29.4.0",
 | 
				
			||||||
 | 
					    "@types/node": "^16.11.58",
 | 
				
			||||||
 | 
					    "@types/react": "^18.0.18",
 | 
				
			||||||
 | 
					    "@types/react-dom": "^18.0.6",
 | 
				
			||||||
 | 
					    "@types/react-measure": "^2.0.6",
 | 
				
			||||||
 | 
					    "@typescript-eslint/eslint-plugin": "^5.49.0",
 | 
				
			||||||
 | 
					    "@typescript-eslint/parser": "^5.49.0",
 | 
				
			||||||
 | 
					    "css-loader": "^6.7.3",
 | 
				
			||||||
 | 
					    "eslint": "^8.23.0",
 | 
				
			||||||
 | 
					    "eslint-config-prettier": "^8.5.0",
 | 
				
			||||||
 | 
					    "eslint-plugin-prettier": "^4.2.1",
 | 
				
			||||||
 | 
					    "eslint-plugin-react": "^7.31.8",
 | 
				
			||||||
 | 
					    "eslint-plugin-react-hooks": "^4.6.0",
 | 
				
			||||||
 | 
					    "html-webpack-plugin": "^5.5.0",
 | 
				
			||||||
 | 
					    "jest": "^29.4.0",
 | 
				
			||||||
 | 
					    "jest-environment-jsdom": "^29.4.0",
 | 
				
			||||||
 | 
					    "prettier": "^2.0.5",
 | 
				
			||||||
 | 
					    "style-loader": "^3.3.1",
 | 
				
			||||||
 | 
					    "ts-jest": "29.0.5",
 | 
				
			||||||
 | 
					    "ts-loader": "^9.4.2",
 | 
				
			||||||
 | 
					    "typescript": "^4.9.4",
 | 
				
			||||||
 | 
					    "webpack": "^5.76.0",
 | 
				
			||||||
 | 
					    "webpack-cli": "^5.0.1",
 | 
				
			||||||
 | 
					    "webpack-dev-server": "^4.11.1",
 | 
				
			||||||
 | 
					    "whatwg-fetch": "^3.6.2"
 | 
				
			||||||
 | 
					  },
 | 
				
			||||||
 | 
					  "lint-staged": {
 | 
				
			||||||
 | 
					    "src/**/*.{ts,tsx,json,css}": [
 | 
				
			||||||
 | 
					      "pretty:fix"
 | 
				
			||||||
 | 
					    ]
 | 
				
			||||||
 | 
					  },
 | 
				
			||||||
 | 
					  "browserslist": {
 | 
				
			||||||
 | 
					    "production": [
 | 
				
			||||||
 | 
					      "chrome >= 67",
 | 
				
			||||||
 | 
					      "edge >= 79",
 | 
				
			||||||
 | 
					      "firefox >= 68",
 | 
				
			||||||
 | 
					      "opera >= 54",
 | 
				
			||||||
 | 
					      "safari >= 14"
 | 
				
			||||||
 | 
					    ],
 | 
				
			||||||
 | 
					    "development": [
 | 
				
			||||||
 | 
					      "last 1 chrome version",
 | 
				
			||||||
 | 
					      "last 1 firefox version",
 | 
				
			||||||
 | 
					      "last 1 safari version"
 | 
				
			||||||
 | 
					    ]
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
							
								
								
									
										9
									
								
								setupJest.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						@ -0,0 +1,9 @@
 | 
				
			|||||||
 | 
					// This file runs some code before a jest test.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// polyfill TextEncoder/TextDecoder since they aren't in jsdom:
 | 
				
			||||||
 | 
					const { TextEncoder, TextDecoder } = require('util');
 | 
				
			||||||
 | 
					global.TextEncoder = TextEncoder;
 | 
				
			||||||
 | 
					global.TextDecoder = TextDecoder;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// polyfill fetch since it's not in jsdom:
 | 
				
			||||||
 | 
					require('whatwg-fetch');
 | 
				
			||||||
							
								
								
									
										52
									
								
								src/App.css
									
									
									
									
									
										Normal file
									
								
							
							
						
						@ -0,0 +1,52 @@
 | 
				
			|||||||
 | 
					.App {
 | 
				
			||||||
 | 
					  text-align: left;
 | 
				
			||||||
 | 
					  font-family: 'Ubuntu', sans-serif;
 | 
				
			||||||
 | 
					  font-size: 16px;
 | 
				
			||||||
 | 
					  min-width: 1350px;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					.App-no-telemetry {
 | 
				
			||||||
 | 
					  height: 100vh;
 | 
				
			||||||
 | 
					  min-width: 0px;
 | 
				
			||||||
 | 
					  font-size: 56px;
 | 
				
			||||||
 | 
					  text-align: center;
 | 
				
			||||||
 | 
					  color: #fff;
 | 
				
			||||||
 | 
					  display: flex;
 | 
				
			||||||
 | 
					  flex-direction: column;
 | 
				
			||||||
 | 
					  justify-content: center;
 | 
				
			||||||
 | 
					  align-items: center;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					.App-no-telemetry-spinner {
 | 
				
			||||||
 | 
					  position: relative;
 | 
				
			||||||
 | 
					  width: 100%;
 | 
				
			||||||
 | 
					  height: 300px;
 | 
				
			||||||
 | 
					  min-height: 240px;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					.App-no-telemetry-spinner > img {
 | 
				
			||||||
 | 
					  width: 300px;
 | 
				
			||||||
 | 
					  height: 300px;
 | 
				
			||||||
 | 
					  min-height: 240px;
 | 
				
			||||||
 | 
					  min-width: 240px;
 | 
				
			||||||
 | 
					  left: 50%;
 | 
				
			||||||
 | 
					  position: absolute;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					.App-no-telemetry-spinner > img:nth-of-type(1) {
 | 
				
			||||||
 | 
					  animation: spin-clockwise 7s linear infinite;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					.App-no-telemetry-spinner > img:nth-of-type(2) {
 | 
				
			||||||
 | 
					  animation: spin-anticlockwise 7s linear infinite;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					@keyframes spin-clockwise {
 | 
				
			||||||
 | 
					  0% { transform: translateX(-50%) rotate(0deg); }
 | 
				
			||||||
 | 
					  100% { transform: translateX(-50%) rotate(360deg); }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					@keyframes spin-anticlockwise {
 | 
				
			||||||
 | 
					  0% { transform: translateX(-50%) rotate(360deg); }
 | 
				
			||||||
 | 
					  100% { transform: translateX(-50%) rotate(0deg); }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
							
								
								
									
										11
									
								
								src/App.test.tsx
									
									
									
									
									
										Normal file
									
								
							
							
						
						@ -0,0 +1,11 @@
 | 
				
			|||||||
 | 
					import * as React from 'react';
 | 
				
			||||||
 | 
					import { createRoot } from 'react-dom/client';
 | 
				
			||||||
 | 
					import App from './App';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					describe('App.tsx', () => {
 | 
				
			||||||
 | 
					  it('renders without crashing', () => {
 | 
				
			||||||
 | 
					    const div = document.createElement('div');
 | 
				
			||||||
 | 
					    const root = createRoot(div);
 | 
				
			||||||
 | 
					    root.render(<App />);
 | 
				
			||||||
 | 
					  });
 | 
				
			||||||
 | 
					});
 | 
				
			||||||
							
								
								
									
										236
									
								
								src/App.tsx
									
									
									
									
									
										Normal file
									
								
							
							
						
						@ -0,0 +1,236 @@
 | 
				
			|||||||
 | 
					import * as React from 'react';
 | 
				
			||||||
 | 
					import { Types, SortedCollection, Maybe, Compare } from './common';
 | 
				
			||||||
 | 
					import { Chain, Ago } from './components';
 | 
				
			||||||
 | 
					import { Row, Column } from './components/List';
 | 
				
			||||||
 | 
					import { Connection } from './Connection';
 | 
				
			||||||
 | 
					import { Persistent, PersistentObject, PersistentSet } from './persist';
 | 
				
			||||||
 | 
					import {
 | 
				
			||||||
 | 
					  bindState,
 | 
				
			||||||
 | 
					  State,
 | 
				
			||||||
 | 
					  Update,
 | 
				
			||||||
 | 
					  Node,
 | 
				
			||||||
 | 
					  ChainData,
 | 
				
			||||||
 | 
					  comparePinnedChains,
 | 
				
			||||||
 | 
					  StateSettings,
 | 
				
			||||||
 | 
					} from './state';
 | 
				
			||||||
 | 
					import { getHashData } from './utils';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					import innerCircleIcon from './assets/inner-circle.svg';
 | 
				
			||||||
 | 
					import outerCircleIcon from './assets/outer-circle.svg';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					import './App.css';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export default class App extends React.Component {
 | 
				
			||||||
 | 
					  private chainsCache: ChainData[] = [];
 | 
				
			||||||
 | 
					  // Custom state for finer control over updates
 | 
				
			||||||
 | 
					  private readonly appState: Readonly<State>;
 | 
				
			||||||
 | 
					  private readonly appUpdate: Update;
 | 
				
			||||||
 | 
					  private readonly settings: PersistentObject<StateSettings>;
 | 
				
			||||||
 | 
					  private readonly pins: PersistentSet<Types.NodeName>;
 | 
				
			||||||
 | 
					  private readonly sortBy: Persistent<Maybe<number>>;
 | 
				
			||||||
 | 
					  private readonly connection: Promise<Connection>;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  constructor(props: Record<string, unknown>) {
 | 
				
			||||||
 | 
					    super(props);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    //todo! Check what's important to set to true, false
 | 
				
			||||||
 | 
					    this.settings = new PersistentObject(
 | 
				
			||||||
 | 
					      'settings',
 | 
				
			||||||
 | 
					      {
 | 
				
			||||||
 | 
					        validator: true,
 | 
				
			||||||
 | 
					        location: true,
 | 
				
			||||||
 | 
					        implementation: true,
 | 
				
			||||||
 | 
					        networkId: true,
 | 
				
			||||||
 | 
					        peers: true,
 | 
				
			||||||
 | 
					        txs: true,
 | 
				
			||||||
 | 
					        upload: false,
 | 
				
			||||||
 | 
					        download: false,
 | 
				
			||||||
 | 
					        stateCacheSize: false,
 | 
				
			||||||
 | 
					        dbCacheSize: false,
 | 
				
			||||||
 | 
					        diskRead: false,
 | 
				
			||||||
 | 
					        diskWrite: false,
 | 
				
			||||||
 | 
					        blocknumber: true,
 | 
				
			||||||
 | 
					        blockhash: true,
 | 
				
			||||||
 | 
					        finalized: false,
 | 
				
			||||||
 | 
					        finalizedhash: false,
 | 
				
			||||||
 | 
					        blocktime: true,
 | 
				
			||||||
 | 
					        blockpropagation: true,
 | 
				
			||||||
 | 
					        blocklasttime: false,
 | 
				
			||||||
 | 
					        uptime: false,
 | 
				
			||||||
 | 
					        version: false,
 | 
				
			||||||
 | 
					        target_os: false,
 | 
				
			||||||
 | 
					        target_arch: false,
 | 
				
			||||||
 | 
					        cpu: false,
 | 
				
			||||||
 | 
					        cpu_hashrate_score: true,
 | 
				
			||||||
 | 
					        cpu_vendor: true,
 | 
				
			||||||
 | 
					        core_count: false,
 | 
				
			||||||
 | 
					        mem: true,
 | 
				
			||||||
 | 
					        memory: false,
 | 
				
			||||||
 | 
					        linux_distro: false,
 | 
				
			||||||
 | 
					        linux_kernel: false,
 | 
				
			||||||
 | 
					        memory_memcpy_score: true,
 | 
				
			||||||
 | 
					        disk_sequential_write_score: true,
 | 
				
			||||||
 | 
					        disk_random_write_score: true,
 | 
				
			||||||
 | 
					        is_virtual_machine: false,
 | 
				
			||||||
 | 
					      },
 | 
				
			||||||
 | 
					      (settings) => {
 | 
				
			||||||
 | 
					        const selectedColumns = this.selectedColumns(settings);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        this.sortBy.set(null);
 | 
				
			||||||
 | 
					        this.appUpdate({ settings, selectedColumns, sortBy: null });
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					    );
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    this.pins = new PersistentSet<Types.NodeName>('pinned_names', (pins) => {
 | 
				
			||||||
 | 
					      const { nodes } = this.appState;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      nodes.mutEachAndSort((node) => node.setPinned(pins.has(node.name)));
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      this.appUpdate({ nodes, pins });
 | 
				
			||||||
 | 
					    });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    this.sortBy = new Persistent<Maybe<number>>('sortBy', null, (sortBy) => {
 | 
				
			||||||
 | 
					      const compare = this.getComparator(sortBy);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      this.appState.nodes.setComparator(compare);
 | 
				
			||||||
 | 
					      this.appUpdate({ sortBy });
 | 
				
			||||||
 | 
					    });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    const { tab = '' } = getHashData();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    this.appUpdate = bindState(this, {
 | 
				
			||||||
 | 
					      status: 'offline',
 | 
				
			||||||
 | 
					      best: 0 as Types.BlockNumber,
 | 
				
			||||||
 | 
					      finalized: 0 as Types.BlockNumber,
 | 
				
			||||||
 | 
					      blockTimestamp: 0 as Types.Timestamp,
 | 
				
			||||||
 | 
					      blockAverage: null,
 | 
				
			||||||
 | 
					      timeDiff: 0 as Types.Milliseconds,
 | 
				
			||||||
 | 
					      subscribed: null,
 | 
				
			||||||
 | 
					      chains: new Map(),
 | 
				
			||||||
 | 
					      nodes: new SortedCollection(Node.compare),
 | 
				
			||||||
 | 
					      settings: this.settings.raw(),
 | 
				
			||||||
 | 
					      pins: this.pins.get(),
 | 
				
			||||||
 | 
					      sortBy: this.sortBy.get(),
 | 
				
			||||||
 | 
					      selectedColumns: this.selectedColumns(this.settings.raw()),
 | 
				
			||||||
 | 
					      tab,
 | 
				
			||||||
 | 
					      chainStats: null,
 | 
				
			||||||
 | 
					    });
 | 
				
			||||||
 | 
					    this.appState = this.appUpdate({});
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    const comparator = this.getComparator(this.sortBy.get());
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    this.appState.nodes.setComparator(comparator);
 | 
				
			||||||
 | 
					    this.connection = Connection.create(
 | 
				
			||||||
 | 
					      this.pins,
 | 
				
			||||||
 | 
					      this.appState,
 | 
				
			||||||
 | 
					      this.appUpdate
 | 
				
			||||||
 | 
					    );
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    setInterval(() => (this.chainsCache = []), 10000);
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  public render() {
 | 
				
			||||||
 | 
					    const { timeDiff, subscribed, status, tab } = this.appState;
 | 
				
			||||||
 | 
					    const chains = this.chains();
 | 
				
			||||||
 | 
					    const subscribedData = subscribed
 | 
				
			||||||
 | 
					      ? this.appState.chains.get(subscribed)
 | 
				
			||||||
 | 
					      : null;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    Ago.timeDiff = timeDiff;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    if (chains.length === 0) {
 | 
				
			||||||
 | 
					      return (
 | 
				
			||||||
 | 
					        <div className="App App-no-telemetry">
 | 
				
			||||||
 | 
					          <div className="App-no-telemetry-spinner">
 | 
				
			||||||
 | 
					            <img src={innerCircleIcon} />
 | 
				
			||||||
 | 
					            <img src={outerCircleIcon} />
 | 
				
			||||||
 | 
					          </div>
 | 
				
			||||||
 | 
					          <div className="App-no-telemetry-text">
 | 
				
			||||||
 | 
					            Waiting for Ghosties…
 | 
				
			||||||
 | 
					          </div>
 | 
				
			||||||
 | 
					        </div>
 | 
				
			||||||
 | 
					      );
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    return (
 | 
				
			||||||
 | 
					      <div className="App">
 | 
				
			||||||
 | 
					        <Chain
 | 
				
			||||||
 | 
					          appState={this.appState}
 | 
				
			||||||
 | 
					          appUpdate={this.appUpdate}
 | 
				
			||||||
 | 
					          connection={this.connection}
 | 
				
			||||||
 | 
					          settings={this.settings}
 | 
				
			||||||
 | 
					          pins={this.pins}
 | 
				
			||||||
 | 
					          sortBy={this.sortBy}
 | 
				
			||||||
 | 
					        />
 | 
				
			||||||
 | 
					      </div>
 | 
				
			||||||
 | 
					    );
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  public componentDidMount() {
 | 
				
			||||||
 | 
					    window.addEventListener('hashchange', this.onHashChange);
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  public componentWillUnmount() {
 | 
				
			||||||
 | 
					    window.removeEventListener('hashchange', this.onHashChange);
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  private onHashChange = () => {
 | 
				
			||||||
 | 
					    const { tab = '' } = getHashData();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    this.appUpdate({ tab });
 | 
				
			||||||
 | 
					  };
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  private chains(): ChainData[] {
 | 
				
			||||||
 | 
					    if (this.chainsCache.length === this.appState.chains.size) {
 | 
				
			||||||
 | 
					      return this.chainsCache;
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    this.chainsCache = Array.from(this.appState.chains.values()).sort(
 | 
				
			||||||
 | 
					      (a, b) => {
 | 
				
			||||||
 | 
					        const pinned = comparePinnedChains(a.genesisHash, b.genesisHash);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        if (pinned !== 0) {
 | 
				
			||||||
 | 
					          return pinned;
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        return b.nodeCount - a.nodeCount;
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					    );
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    return this.chainsCache;
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  private selectedColumns(settings: StateSettings): Column[] {
 | 
				
			||||||
 | 
					    return Row.columns.filter(
 | 
				
			||||||
 | 
					      ({ setting }) => setting == null || settings[setting]
 | 
				
			||||||
 | 
					    );
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  private getComparator(sortBy: Maybe<number>): Compare<Node> {
 | 
				
			||||||
 | 
					    const columns = this.appState.selectedColumns;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    if (sortBy != null) {
 | 
				
			||||||
 | 
					      const [index, rev] = sortBy < 0 ? [~sortBy, -1] : [sortBy, 1];
 | 
				
			||||||
 | 
					      const column = columns[index];
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      if (column != null && column.sortBy) {
 | 
				
			||||||
 | 
					        const key = column.sortBy;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        return (a, b) => {
 | 
				
			||||||
 | 
					          const aKey = key(a);
 | 
				
			||||||
 | 
					          const bKey = key(b);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					          if (aKey < bKey) {
 | 
				
			||||||
 | 
					            return -1 * rev;
 | 
				
			||||||
 | 
					          } else if (aKey > bKey) {
 | 
				
			||||||
 | 
					            return 1 * rev;
 | 
				
			||||||
 | 
					          } else {
 | 
				
			||||||
 | 
					            return Node.compare(a, b);
 | 
				
			||||||
 | 
					          }
 | 
				
			||||||
 | 
					        };
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    return Node.compare;
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
							
								
								
									
										540
									
								
								src/Connection.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						@ -0,0 +1,540 @@
 | 
				
			|||||||
 | 
					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 { ACTIONS } from './common/feed';
 | 
				
			||||||
 | 
					import {
 | 
				
			||||||
 | 
					  Column,
 | 
				
			||||||
 | 
					  LocationColumn,
 | 
				
			||||||
 | 
					  PeersColumn,
 | 
				
			||||||
 | 
					  TxsColumn,
 | 
				
			||||||
 | 
					  FinalizedBlockColumn,
 | 
				
			||||||
 | 
					  FinalizedHashColumn,
 | 
				
			||||||
 | 
					  UploadColumn,
 | 
				
			||||||
 | 
					  DownloadColumn,
 | 
				
			||||||
 | 
					  StateCacheColumn,
 | 
				
			||||||
 | 
					} from './components/List';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					const CONNECTION_TIMEOUT_BASE = (1000 * 5) as Types.Milliseconds; // 5 seconds
 | 
				
			||||||
 | 
					const CONNECTION_TIMEOUT_MAX = (1000 * 60 * 5) as Types.Milliseconds; // 5 minutes
 | 
				
			||||||
 | 
					const MESSAGE_TIMEOUT = (1000 * 60) as Types.Milliseconds; // 60 seconds
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					declare global {
 | 
				
			||||||
 | 
					  interface Window {
 | 
				
			||||||
 | 
					    process_env: string;
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export class Connection {
 | 
				
			||||||
 | 
					  public static async create(
 | 
				
			||||||
 | 
					    pins: PersistentSet<Types.NodeName>,
 | 
				
			||||||
 | 
					    appState: Readonly<State>,
 | 
				
			||||||
 | 
					    appUpdate: Update
 | 
				
			||||||
 | 
					  ): Promise<Connection> {
 | 
				
			||||||
 | 
					    return new Connection(await Connection.socket(), appState, appUpdate, pins);
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  private static readonly utf8decoder = new TextDecoder('utf-8');
 | 
				
			||||||
 | 
					  private static readonly address = Connection.getAddress();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  private static getAddress(): string {
 | 
				
			||||||
 | 
					    const ENV_URL = 'SUBSTRATE_TELEMETRY_URL';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    // If env_config.js is generated and loaded in, it'll set this variable.
 | 
				
			||||||
 | 
					    // This is set up in the Dockerfile. Otherwise, we just connect to a
 | 
				
			||||||
 | 
					    // default URL.
 | 
				
			||||||
 | 
					    if (window.process_env?.[ENV_URL]) {
 | 
				
			||||||
 | 
					      return window.process_env[ENV_URL] as string;
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    if (window.location.protocol === 'https:') {
 | 
				
			||||||
 | 
					      return `wss://${window.location.hostname}/feed/`;
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					    
 | 
				
			||||||
 | 
					    return 'ws://127.0.0.1:8000/feed';
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  private static async socket(): Promise<WebSocket> {
 | 
				
			||||||
 | 
					    let socket = await Connection.trySocket();
 | 
				
			||||||
 | 
					    let timeout = CONNECTION_TIMEOUT_BASE;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    while (!socket) {
 | 
				
			||||||
 | 
					      await sleep(timeout);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      timeout = Math.min(
 | 
				
			||||||
 | 
					        timeout * 2,
 | 
				
			||||||
 | 
					        CONNECTION_TIMEOUT_MAX
 | 
				
			||||||
 | 
					      ) as Types.Milliseconds;
 | 
				
			||||||
 | 
					      socket = await Connection.trySocket();
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    return socket;
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  private static async trySocket(): Promise<Maybe<WebSocket>> {
 | 
				
			||||||
 | 
					    return new Promise<Maybe<WebSocket>>((resolve) => {
 | 
				
			||||||
 | 
					      function clean() {
 | 
				
			||||||
 | 
					        socket.removeEventListener('open', onSuccess);
 | 
				
			||||||
 | 
					        socket.removeEventListener('close', onFailure);
 | 
				
			||||||
 | 
					        socket.removeEventListener('error', onFailure);
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      function onSuccess() {
 | 
				
			||||||
 | 
					        clean();
 | 
				
			||||||
 | 
					        resolve(socket);
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      function onFailure() {
 | 
				
			||||||
 | 
					        clean();
 | 
				
			||||||
 | 
					        resolve(null);
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					      const socket = new WebSocket(Connection.address);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      socket.binaryType = 'arraybuffer';
 | 
				
			||||||
 | 
					      socket.addEventListener('open', onSuccess);
 | 
				
			||||||
 | 
					      socket.addEventListener('error', onFailure);
 | 
				
			||||||
 | 
					      socket.addEventListener('close', onFailure);
 | 
				
			||||||
 | 
					    });
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  // timer which will force a reconnection if no message is seen for a while
 | 
				
			||||||
 | 
					  private messageTimeout: Maybe<ResettableTimeout> = null;
 | 
				
			||||||
 | 
					  // id sent to the backend used to pair responses
 | 
				
			||||||
 | 
					  private pingId = 0;
 | 
				
			||||||
 | 
					  // timeout handler for ping messages
 | 
				
			||||||
 | 
					  private pingTimeout: NodeJS.Timer;
 | 
				
			||||||
 | 
					  // timestamp at which the last ping has been sent
 | 
				
			||||||
 | 
					  private pingSent: Maybe<Types.Timestamp> = null;
 | 
				
			||||||
 | 
					  // chain label to resubsribe to on reconnect
 | 
				
			||||||
 | 
					  private resubscribeTo: Maybe<Types.GenesisHash> = getHashData().chain;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  constructor(
 | 
				
			||||||
 | 
					    private socket: WebSocket,
 | 
				
			||||||
 | 
					    private readonly appState: Readonly<State>,
 | 
				
			||||||
 | 
					    private readonly appUpdate: Update,
 | 
				
			||||||
 | 
					    private readonly pins: PersistentSet<Types.NodeName>
 | 
				
			||||||
 | 
					  ) {
 | 
				
			||||||
 | 
					    this.bindSocket();
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  public subscribe(chain: Types.GenesisHash) {
 | 
				
			||||||
 | 
					    if (
 | 
				
			||||||
 | 
					      this.appState.subscribed != null &&
 | 
				
			||||||
 | 
					      this.appState.subscribed !== chain
 | 
				
			||||||
 | 
					    ) {
 | 
				
			||||||
 | 
					      this.appUpdate({
 | 
				
			||||||
 | 
					        tab: 'list',
 | 
				
			||||||
 | 
					      });
 | 
				
			||||||
 | 
					      setHashData({ chain, tab: 'list' });
 | 
				
			||||||
 | 
					    } else {
 | 
				
			||||||
 | 
					      setHashData({ chain });
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    this.socket.send(`subscribe:${chain}`);
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  private handleMessages = (messages: FeedMessage.Message[]) => {
 | 
				
			||||||
 | 
					    this.messageTimeout?.reset();
 | 
				
			||||||
 | 
					    const { nodes, chains, sortBy, selectedColumns } = this.appState;
 | 
				
			||||||
 | 
					    const nodesStateRef = nodes.ref;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    let sortByColumn: Maybe<Column> = null;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    if (sortBy != null) {
 | 
				
			||||||
 | 
					      sortByColumn =
 | 
				
			||||||
 | 
					        sortBy < 0 ? selectedColumns[~sortBy] : selectedColumns[sortBy];
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    for (const message of messages) {
 | 
				
			||||||
 | 
					      switch (message.action) {
 | 
				
			||||||
 | 
					        case ACTIONS.FeedVersion: {
 | 
				
			||||||
 | 
					          if (message.payload !== VERSION) {
 | 
				
			||||||
 | 
					            return this.newVersion();
 | 
				
			||||||
 | 
					          }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					          break;
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        case ACTIONS.BestBlock: {
 | 
				
			||||||
 | 
					          const [best, blockTimestamp, blockAverage] = message.payload;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					          nodes.mutEach((node) => node.newBestBlock());
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					          this.appUpdate({ best, blockTimestamp, blockAverage });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					          break;
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        case ACTIONS.BestFinalized: {
 | 
				
			||||||
 | 
					          const [finalized /*, hash */] = message.payload;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					          this.appUpdate({ finalized });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					          break;
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        case ACTIONS.AddedNode: {
 | 
				
			||||||
 | 
					          const [
 | 
				
			||||||
 | 
					            id,
 | 
				
			||||||
 | 
					            nodeDetails,
 | 
				
			||||||
 | 
					            nodeStats,
 | 
				
			||||||
 | 
					            nodeIO,
 | 
				
			||||||
 | 
					            nodeHardware,
 | 
				
			||||||
 | 
					            blockDetails,
 | 
				
			||||||
 | 
					            location,
 | 
				
			||||||
 | 
					            startupTime,
 | 
				
			||||||
 | 
					          ] = message.payload;
 | 
				
			||||||
 | 
					          const pinned = this.pins.has(nodeDetails[0]);
 | 
				
			||||||
 | 
					          const node = new Node(
 | 
				
			||||||
 | 
					            pinned,
 | 
				
			||||||
 | 
					            id,
 | 
				
			||||||
 | 
					            nodeDetails,
 | 
				
			||||||
 | 
					            nodeStats,
 | 
				
			||||||
 | 
					            nodeIO,
 | 
				
			||||||
 | 
					            nodeHardware,
 | 
				
			||||||
 | 
					            blockDetails,
 | 
				
			||||||
 | 
					            location,
 | 
				
			||||||
 | 
					            startupTime
 | 
				
			||||||
 | 
					          );
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					          nodes.add(node);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					          break;
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        case ACTIONS.RemovedNode: {
 | 
				
			||||||
 | 
					          const id = message.payload;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					          nodes.remove(id);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					          break;
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        case ACTIONS.StaleNode: {
 | 
				
			||||||
 | 
					          const id = message.payload;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					          nodes.mutAndSort(id, (node) => node.setStale(true));
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					          break;
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        case ACTIONS.LocatedNode: {
 | 
				
			||||||
 | 
					          const [id, lat, lon, city] = message.payload;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					          nodes.mutAndMaybeSort(
 | 
				
			||||||
 | 
					            id,
 | 
				
			||||||
 | 
					            (node) => node.updateLocation([lat, lon, city]),
 | 
				
			||||||
 | 
					            sortByColumn === LocationColumn
 | 
				
			||||||
 | 
					          );
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					          break;
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        case ACTIONS.ImportedBlock: {
 | 
				
			||||||
 | 
					          const [id, blockDetails] = message.payload;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					          nodes.mutAndSort(id, (node) => node.updateBlock(blockDetails));
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					          break;
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        case ACTIONS.FinalizedBlock: {
 | 
				
			||||||
 | 
					          const [id, height, hash] = message.payload;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					          nodes.mutAndMaybeSort(
 | 
				
			||||||
 | 
					            id,
 | 
				
			||||||
 | 
					            (node) => node.updateFinalized(height, hash),
 | 
				
			||||||
 | 
					            sortByColumn === FinalizedBlockColumn ||
 | 
				
			||||||
 | 
					              sortByColumn === FinalizedHashColumn
 | 
				
			||||||
 | 
					          );
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					          break;
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        case ACTIONS.NodeStats: {
 | 
				
			||||||
 | 
					          const [id, nodeStats] = message.payload;
 | 
				
			||||||
 | 
					          nodes.mutAndMaybeSort(
 | 
				
			||||||
 | 
					            id,
 | 
				
			||||||
 | 
					            (node) => node.updateStats(nodeStats),
 | 
				
			||||||
 | 
					            sortByColumn === PeersColumn || sortByColumn === TxsColumn
 | 
				
			||||||
 | 
					          );
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					          break;
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        case ACTIONS.NodeHardware: {
 | 
				
			||||||
 | 
					          const [id, nodeHardware] = message.payload;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					          nodes.mutAndMaybeSort(
 | 
				
			||||||
 | 
					            id,
 | 
				
			||||||
 | 
					            (node) => node.updateHardware(nodeHardware),
 | 
				
			||||||
 | 
					            sortByColumn === UploadColumn || sortByColumn === DownloadColumn
 | 
				
			||||||
 | 
					          );
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					          break;
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        case ACTIONS.NodeIO: {
 | 
				
			||||||
 | 
					          const [id, nodeIO] = message.payload;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					          nodes.mutAndMaybeSort(
 | 
				
			||||||
 | 
					            id,
 | 
				
			||||||
 | 
					            (node) => node.updateIO(nodeIO),
 | 
				
			||||||
 | 
					            sortByColumn === StateCacheColumn
 | 
				
			||||||
 | 
					          );
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					          break;
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        case ACTIONS.TimeSync: {
 | 
				
			||||||
 | 
					          this.appUpdate({
 | 
				
			||||||
 | 
					            timeDiff: (timestamp() - message.payload) as Types.Milliseconds,
 | 
				
			||||||
 | 
					          });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					          break;
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        case ACTIONS.AddedChain: {
 | 
				
			||||||
 | 
					          const [label, genesisHash, nodeCount] = message.payload;
 | 
				
			||||||
 | 
					          const chain = chains.get(genesisHash);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					          if (chain) {
 | 
				
			||||||
 | 
					            chain.nodeCount = nodeCount;
 | 
				
			||||||
 | 
					          } else {
 | 
				
			||||||
 | 
					            chains.set(genesisHash, { label, genesisHash, nodeCount });
 | 
				
			||||||
 | 
					          }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					          this.appUpdate({ chains });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					          break;
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        case ACTIONS.RemovedChain: {
 | 
				
			||||||
 | 
					          chains.delete(message.payload);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					          if (this.appState.subscribed === message.payload) {
 | 
				
			||||||
 | 
					            nodes.clear();
 | 
				
			||||||
 | 
					            this.appUpdate({ subscribed: null, nodes, chains });
 | 
				
			||||||
 | 
					          }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					          break;
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        case ACTIONS.SubscribedTo: {
 | 
				
			||||||
 | 
					          nodes.clear();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					          this.appUpdate({ subscribed: message.payload, nodes });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					          break;
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        case ACTIONS.UnsubscribedFrom: {
 | 
				
			||||||
 | 
					          if (this.appState.subscribed === message.payload) {
 | 
				
			||||||
 | 
					            nodes.clear();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            this.appUpdate({ subscribed: null, nodes });
 | 
				
			||||||
 | 
					          }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					          break;
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        case ACTIONS.Pong: {
 | 
				
			||||||
 | 
					          this.pong(Number(message.payload));
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					          break;
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        case ACTIONS.ChainStatsUpdate: {
 | 
				
			||||||
 | 
					          this.appUpdate({ chainStats: message.payload });
 | 
				
			||||||
 | 
					          break;
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        default: {
 | 
				
			||||||
 | 
					          break;
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    if (nodes.hasChangedSince(nodesStateRef)) {
 | 
				
			||||||
 | 
					      this.appUpdate({ nodes });
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    this.autoSubscribe();
 | 
				
			||||||
 | 
					  };
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  private bindSocket() {
 | 
				
			||||||
 | 
					    console.log('Connected');
 | 
				
			||||||
 | 
					    // Disconnect if no messages are received in 60s:
 | 
				
			||||||
 | 
					    this.messageTimeout = resettableTimeout(
 | 
				
			||||||
 | 
					      this.handleDisconnect,
 | 
				
			||||||
 | 
					      MESSAGE_TIMEOUT
 | 
				
			||||||
 | 
					    );
 | 
				
			||||||
 | 
					    // Ping periodically to keep the above happy even if no other data is coming in:
 | 
				
			||||||
 | 
					    this.ping();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    if (this.appState) {
 | 
				
			||||||
 | 
					      const { nodes } = this.appState;
 | 
				
			||||||
 | 
					      nodes.clear();
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    this.appUpdate({
 | 
				
			||||||
 | 
					      status: 'online',
 | 
				
			||||||
 | 
					    });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    if (this.appState.subscribed) {
 | 
				
			||||||
 | 
					      this.resubscribeTo = this.appState.subscribed;
 | 
				
			||||||
 | 
					      this.appUpdate({ subscribed: null });
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    this.socket.addEventListener('message', this.handleFeedData);
 | 
				
			||||||
 | 
					    this.socket.addEventListener('close', this.handleDisconnect);
 | 
				
			||||||
 | 
					    this.socket.addEventListener('error', this.handleDisconnect);
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  private ping = () => {
 | 
				
			||||||
 | 
					    if (this.pingSent) {
 | 
				
			||||||
 | 
					      this.handleDisconnect();
 | 
				
			||||||
 | 
					      return;
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    this.pingId += 1;
 | 
				
			||||||
 | 
					    this.pingSent = timestamp();
 | 
				
			||||||
 | 
					    this.socket.send(`ping:${this.pingId}`);
 | 
				
			||||||
 | 
					  };
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  private pong(id: number) {
 | 
				
			||||||
 | 
					    if (!this.pingSent) {
 | 
				
			||||||
 | 
					      console.error('Received a pong without sending a ping first');
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      this.handleDisconnect();
 | 
				
			||||||
 | 
					      return;
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    if (id !== this.pingId) {
 | 
				
			||||||
 | 
					      console.error('pingId differs');
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      this.handleDisconnect();
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    const latency = timestamp() - this.pingSent;
 | 
				
			||||||
 | 
					    console.log(`Ping latency: ${latency}ms`);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    this.pingSent = null;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    // Schedule a new ping to be sent at least 30s after the last one:
 | 
				
			||||||
 | 
					    this.pingTimeout = setTimeout(
 | 
				
			||||||
 | 
					      this.ping,
 | 
				
			||||||
 | 
					      Math.max(0, MESSAGE_TIMEOUT / 2 - latency)
 | 
				
			||||||
 | 
					    );
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  private newVersion() {
 | 
				
			||||||
 | 
					    this.appUpdate({ status: 'upgrade-requested' });
 | 
				
			||||||
 | 
					    this.clean();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    // Force reload from the server
 | 
				
			||||||
 | 
					    setTimeout(() => window.location.reload(), 3000);
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  private clean() {
 | 
				
			||||||
 | 
					    clearTimeout(this.pingTimeout);
 | 
				
			||||||
 | 
					    this.pingSent = null;
 | 
				
			||||||
 | 
					    this.messageTimeout?.cancel();
 | 
				
			||||||
 | 
					    this.messageTimeout = null;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    this.socket.removeEventListener('message', this.handleFeedData);
 | 
				
			||||||
 | 
					    this.socket.removeEventListener('close', this.handleDisconnect);
 | 
				
			||||||
 | 
					    this.socket.removeEventListener('error', this.handleDisconnect);
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  private handleFeedData = (event: MessageEvent) => {
 | 
				
			||||||
 | 
					    let data: FeedMessage.Data;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    if (typeof event.data === 'string') {
 | 
				
			||||||
 | 
					      // eslint-disable-next-line @typescript-eslint/no-explicit-any
 | 
				
			||||||
 | 
					      data = event.data as any as FeedMessage.Data;
 | 
				
			||||||
 | 
					    } else {
 | 
				
			||||||
 | 
					      const u8aData = new Uint8Array(event.data);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      // Future-proofing for when we switch to binary feed
 | 
				
			||||||
 | 
					      if (u8aData[0] === 0x00) {
 | 
				
			||||||
 | 
					        return this.newVersion();
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      const str = Connection.utf8decoder.decode(event.data);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      // eslint-disable-next-line @typescript-eslint/no-explicit-any
 | 
				
			||||||
 | 
					      data = str as any as FeedMessage.Data;
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    this.handleMessages(FeedMessage.deserialize(data));
 | 
				
			||||||
 | 
					  };
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  private autoSubscribe() {
 | 
				
			||||||
 | 
					    const { subscribed, chains } = this.appState;
 | 
				
			||||||
 | 
					    const { resubscribeTo } = this;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    if (subscribed) {
 | 
				
			||||||
 | 
					      return;
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    if (resubscribeTo) {
 | 
				
			||||||
 | 
					      if (chains.has(resubscribeTo)) {
 | 
				
			||||||
 | 
					        this.subscribe(resubscribeTo);
 | 
				
			||||||
 | 
					        return;
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    let topChain: Maybe<ChainData> = null;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    for (const chain of chains.values()) {
 | 
				
			||||||
 | 
					      if (PINNED_CHAINS[chain.genesisHash] === 1) {
 | 
				
			||||||
 | 
					        topChain = chain;
 | 
				
			||||||
 | 
					        break;
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      if (!topChain || chain.nodeCount > topChain.nodeCount) {
 | 
				
			||||||
 | 
					        topChain = chain;
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    if (topChain) {
 | 
				
			||||||
 | 
					      this.subscribe(topChain.genesisHash);
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  private handleDisconnect = async () => {
 | 
				
			||||||
 | 
					    console.warn('Disconnecting; will attempt reconnect');
 | 
				
			||||||
 | 
					    this.appUpdate({ status: 'offline' });
 | 
				
			||||||
 | 
					    this.clean();
 | 
				
			||||||
 | 
					    this.socket.close();
 | 
				
			||||||
 | 
					    this.socket = await Connection.socket();
 | 
				
			||||||
 | 
					    this.bindSocket();
 | 
				
			||||||
 | 
					  };
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					/**
 | 
				
			||||||
 | 
					 * Fire a function if the timer runs out. You can reset it, or
 | 
				
			||||||
 | 
					 * cancel it to prevent the function from being fired.
 | 
				
			||||||
 | 
					 *
 | 
				
			||||||
 | 
					 * @param onExpired
 | 
				
			||||||
 | 
					 * @param timeoutMs
 | 
				
			||||||
 | 
					 * @returns
 | 
				
			||||||
 | 
					 */
 | 
				
			||||||
 | 
					function resettableTimeout(
 | 
				
			||||||
 | 
					  onExpired: () => void,
 | 
				
			||||||
 | 
					  timeoutMs: Types.Milliseconds
 | 
				
			||||||
 | 
					) {
 | 
				
			||||||
 | 
					  let timer = setTimeout(onExpired, timeoutMs);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  return {
 | 
				
			||||||
 | 
					    reset() {
 | 
				
			||||||
 | 
					      clearTimeout(timer);
 | 
				
			||||||
 | 
					      timer = setTimeout(onExpired, timeoutMs);
 | 
				
			||||||
 | 
					    },
 | 
				
			||||||
 | 
					    cancel() {
 | 
				
			||||||
 | 
					      clearTimeout(timer);
 | 
				
			||||||
 | 
					    },
 | 
				
			||||||
 | 
					  };
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					type ResettableTimeout = ReturnType<typeof resettableTimeout>;
 | 
				
			||||||
							
								
								
									
										1
									
								
								src/assets/ghost-logo.svg
									
									
									
									
									
										Normal file
									
								
							
							
						
						| 
		 After Width: | Height: | Size: 7.3 KiB  | 
							
								
								
									
										42
									
								
								src/assets/inner-circle.svg
									
									
									
									
									
										Normal file
									
								
							
							
						
						@ -0,0 +1,42 @@
 | 
				
			|||||||
 | 
					<?xml version="1.0" encoding="utf-8"?>
 | 
				
			||||||
 | 
					<!-- Generator: Adobe Illustrator 27.5.0, SVG Export Plug-In . SVG Version: 6.00 Build 0)  -->
 | 
				
			||||||
 | 
					<svg version="1.0" id="Layer_2_00000103961323752282420440000002190430745304150146_"
 | 
				
			||||||
 | 
						 xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px" viewBox="0 0 223 223"
 | 
				
			||||||
 | 
						 style="enable-background:new 0 0 223 223;" xml:space="preserve">
 | 
				
			||||||
 | 
					<g id="Layer_1-2">
 | 
				
			||||||
 | 
						<circle style="fill:none;" cx="111.5" cy="111.5" r="111.5"/>
 | 
				
			||||||
 | 
						
 | 
				
			||||||
 | 
							<linearGradient id="SVGID_1_" gradientUnits="userSpaceOnUse" x1="115.7906" y1="720.4291" x2="34.4315" y2="682.4843" gradientTransform="matrix(1 0 0 1 0 -568)">
 | 
				
			||||||
 | 
							<stop  offset="0" style="stop-color:#FFFFFF;stop-opacity:0"/>
 | 
				
			||||||
 | 
							<stop  offset="0.14" style="stop-color:#FFFFFF;stop-opacity:0.37"/>
 | 
				
			||||||
 | 
							<stop  offset="1" style="stop-color:#FFFFFF"/>
 | 
				
			||||||
 | 
						</linearGradient>
 | 
				
			||||||
 | 
						<path style="fill:url(#SVGID_1_);" d="M101.7,177.2c-0.1,0-0.3,0-0.4,0l-0.4-0.1l-5.3-1.1l-1.7-0.4c-0.4-0.1-0.8-0.2-1.2-0.3
 | 
				
			||||||
 | 
							c-0.8-0.2-1.5-0.4-2.3-0.6l-0.1,0l-3.4-1.1c-4.6-1.6-9-3.7-13.2-6.1c-8.5-5.1-15.7-12.2-21-20.6c-7.8-12.7-11.4-27.6-10.3-42.5
 | 
				
			||||||
 | 
							c0.3-4.3,1.2-8.7,2.5-12.8c1-3,4.3-4.6,7.3-3.6s4.6,4.3,3.6,7.3c0,0,0,0,0,0c0,0.1,0,0.1-0.1,0.2c-1.2,3.2-1.9,6.5-2.2,9.9
 | 
				
			||||||
 | 
							c-0.4,4.2-0.4,8.4,0,12.5c0.8,8.4,3.5,16.5,7.7,23.8c4.3,7.3,10.2,13.4,17.3,18c3.6,2.3,7.4,4.2,11.4,5.7l3.1,1.1
 | 
				
			||||||
 | 
							c0.6,0.2,1.3,0.4,1.9,0.6c0.4,0.1,0.8,0.2,1.2,0.4l1.5,0.4l5,1.2c2.2,0.7,3.4,3,2.8,5.2C105,176,103.5,177.2,101.7,177.2
 | 
				
			||||||
 | 
							L101.7,177.2L101.7,177.2z"/>
 | 
				
			||||||
 | 
						
 | 
				
			||||||
 | 
							<linearGradient id="SVGID_00000024700464465339729840000004120134249967546251_" gradientUnits="userSpaceOnUse" x1="82.875" y1="667.6512" x2="139.7121" y2="610.8142" gradientTransform="matrix(1 0 0 1 0 -568)">
 | 
				
			||||||
 | 
							<stop  offset="0" style="stop-color:#FFFFFF;stop-opacity:0"/>
 | 
				
			||||||
 | 
							<stop  offset="0.14" style="stop-color:#FFFFFF;stop-opacity:0.37"/>
 | 
				
			||||||
 | 
							<stop  offset="1" style="stop-color:#FFFFFF"/>
 | 
				
			||||||
 | 
						</linearGradient>
 | 
				
			||||||
 | 
						<path style="fill:url(#SVGID_00000024700464465339729840000004120134249967546251_);" d="M159.2,76.9c-1.9,0-3.7-0.9-4.7-2.5
 | 
				
			||||||
 | 
							c-2-3-4.4-5.7-7.2-8c-3.4-2.8-7-5.3-11-7.3c-7.9-4.1-16.6-6.3-25.5-6.7c-4.4-0.2-8.8,0.2-13.2,1.2c-4.3,0.9-8.5,2.4-12.5,4.4
 | 
				
			||||||
 | 
							c-4,2-7.8,4.4-11.3,7.3c-3.5,2.9-6.7,6.1-9.7,9.6l-0.3,0.3c-1.8,1.5-4.4,1.3-5.9-0.4c-1.2-1.3-1.3-3.2-0.5-4.8l0.3-0.4
 | 
				
			||||||
 | 
							c3.2-3.9,6.7-7.6,10.4-11c3.9-3.3,8.2-6.3,12.7-8.7c9.2-4.9,19.5-7.4,30-7.3c10.4,0.1,20.7,2.5,30.1,7c4.7,2.2,9.2,5,13.3,8.3
 | 
				
			||||||
 | 
							c3.6,2.9,6.9,6.3,9.6,10c1.8,2.6,1.2,6.2-1.4,8c-0.1,0.1-0.3,0.2-0.4,0.2C161.2,76.7,160.2,77,159.2,76.9L159.2,76.9z"/>
 | 
				
			||||||
 | 
						
 | 
				
			||||||
 | 
							<linearGradient id="SVGID_00000038414855759987205370000008965600844213966738_" gradientUnits="userSpaceOnUse" x1="149.5093" y1="656.3518" x2="149.5093" y2="745.62" gradientTransform="matrix(1 0 0 1 0 -568)">
 | 
				
			||||||
 | 
							<stop  offset="0" style="stop-color:#FFFFFF;stop-opacity:0"/>
 | 
				
			||||||
 | 
							<stop  offset="0.14" style="stop-color:#FFFFFF;stop-opacity:0.37"/>
 | 
				
			||||||
 | 
							<stop  offset="1" style="stop-color:#FFFFFF"/>
 | 
				
			||||||
 | 
						</linearGradient>
 | 
				
			||||||
 | 
						<path style="fill:url(#SVGID_00000038414855759987205370000008965600844213966738_);" d="M126.8,177.6c-3.2,0-5.7-2.5-5.7-5.7
 | 
				
			||||||
 | 
							c0-2.6,1.7-4.9,4.2-5.5c7-1.9,13.7-5.1,19.6-9.4c6.5-4.6,12-10.5,16.2-17.2c4.2-6.8,6.8-14.4,7.7-22.3c0.8-8.1,0-16.2-2.2-24
 | 
				
			||||||
 | 
							l-0.1-0.3c-0.4-2.3,1.1-4.4,3.4-4.8c1.8-0.3,3.5,0.6,4.4,2.2l0.1,0.3l0.1,0.3c2.8,8.8,4,18,3.3,27.2c-0.7,9.3-3.6,18.4-8.2,26.5
 | 
				
			||||||
 | 
							c-9.2,15.6-23.9,27.2-41.2,32.5C127.9,177.5,127.3,177.6,126.8,177.6z"/>
 | 
				
			||||||
 | 
					</g>
 | 
				
			||||||
 | 
					</svg>
 | 
				
			||||||
| 
		 After Width: | Height: | Size: 3.3 KiB  | 
							
								
								
									
										16
									
								
								src/assets/inner-circle_old.svg
									
									
									
									
									
										Normal file
									
								
							
							
						
						@ -0,0 +1,16 @@
 | 
				
			|||||||
 | 
					<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" viewBox="0 0 223 223">
 | 
				
			||||||
 | 
					  <defs>
 | 
				
			||||||
 | 
					    <style>.a{fill:none;}.b{fill:url(#a);}.c{fill:url(#b);}.d{fill:url(#c);}</style>
 | 
				
			||||||
 | 
					    <linearGradient id="a" x1="115.8069" y1="152.4435" x2="34.4357" y2="114.4995" gradientUnits="userSpaceOnUse">
 | 
				
			||||||
 | 
					      <stop offset="0" stop-color="#fff" stop-opacity="0"/>
 | 
				
			||||||
 | 
					      <stop offset="0.1375" stop-color="#a1a1a1" stop-opacity="0.3685"/>
 | 
				
			||||||
 | 
					      <stop offset="1"/>
 | 
				
			||||||
 | 
					    </linearGradient>
 | 
				
			||||||
 | 
					    <linearGradient id="b" x1="82.8779" y1="99.6568" x2="139.7257" y2="42.809" xlink:href="#a"/>
 | 
				
			||||||
 | 
					    <linearGradient id="c" x1="149.512" y1="88.35" x2="149.512" y2="177.6234" xlink:href="#a"/>
 | 
				
			||||||
 | 
					  </defs>
 | 
				
			||||||
 | 
					  <circle class="a" cx="111.5" cy="111.5" r="111.5"/>
 | 
				
			||||||
 | 
					  <path d="M101.7051,177.1943a4.0012,4.0012,0,0,1-.4022-.02l-.3562-.0528-5.2762-1.0645-1.6672-.4362c-.4082-.11-.788-.2081-1.1678-.3066-.7664-.1976-1.5324-.3961-2.29-.6242l-.0928-.0288-3.3721-1.111a73.5989,73.5989,0,0,1-13.1918-6.1478,63.57,63.57,0,0,1-20.9464-20.5767A71.5082,71.5082,0,0,1,42.63,104.3473,51.4261,51.4261,0,0,1,45.165,91.5641a5.7194,5.7194,0,1,1,10.811,3.7373q-.027.0783-.0564.1559a40.5484,40.5484,0,0,0-2.238,9.9322,58.8594,58.8594,0,0,0,.0417,12.5134,59.6656,59.6656,0,0,0,7.6914,23.79,54.0709,54.0709,0,0,0,17.2938,17.98,64.6922,64.6922,0,0,0,11.3772,5.7013l3.0992,1.12c.63.2081,1.2674.3922,1.9049.5763.4038.1169.808.2337,1.21.3554l1.5.441,5.0341,1.1662a4.1733,4.1733,0,0,1-1.1293,8.1608Z"/>
 | 
				
			||||||
 | 
					  <path d="M159.2084,76.9486a5.6916,5.6916,0,0,1-4.7435-2.5313,38.3223,38.3223,0,0,0-7.2208-7.9928,57.2168,57.2168,0,0,0-10.955-7.28,60.6741,60.6741,0,0,0-25.5179-6.6753A55.1161,55.1161,0,0,0,97.5986,53.617a53.45,53.45,0,0,0-12.49,4.3709,62.2047,62.2047,0,0,0-11.2972,7.2588,80.4544,80.4544,0,0,0-9.6638,9.6006l-.29.3A4.1827,4.1827,0,0,1,57.4664,69.95l.3014-.429a88.9654,88.9654,0,0,1,10.454-10.9527,71.2747,71.2747,0,0,1,12.7435-8.6923A62.6057,62.6057,0,0,1,110.96,42.5647a71.0985,71.0985,0,0,1,30.0809,6.9891,68.1868,68.1868,0,0,1,13.271,8.31,49.6737,49.6737,0,0,1,9.5544,10.0315,5.7332,5.7332,0,0,1-1.35,7.9946q-.1887.1341-.3876.2527A5.6969,5.6969,0,0,1,159.2084,76.9486Z"/>
 | 
				
			||||||
 | 
					  <path d="M126.76,177.6233a5.7144,5.7144,0,0,1-1.52-11.2279,61.3735,61.3735,0,0,0,19.6125-9.3711,60.6979,60.6979,0,0,0,16.2365-17.24,53.5379,53.5379,0,0,0,7.665-22.2911,63.3719,63.3719,0,0,0-2.2267-23.99l-.0677-.2922a4.17,4.17,0,0,1,7.8023-2.6333l.1493.2906.0956.3122a72.0341,72.0341,0,0,1,3.3141,27.1823,63.0029,63.0029,0,0,1-8.2232,26.4915,72.467,72.467,0,0,1-41.174,32.523A5.7222,5.7222,0,0,1,126.76,177.6233Z"/>
 | 
				
			||||||
 | 
					</svg>
 | 
				
			||||||
| 
		 After Width: | Height: | Size: 2.5 KiB  | 
							
								
								
									
										43
									
								
								src/assets/outer-circle.svg
									
									
									
									
									
										Normal file
									
								
							
							
						
						@ -0,0 +1,43 @@
 | 
				
			|||||||
 | 
					<?xml version="1.0" encoding="utf-8"?>
 | 
				
			||||||
 | 
					<!-- Generator: Adobe Illustrator 27.5.0, SVG Export Plug-In . SVG Version: 6.00 Build 0)  -->
 | 
				
			||||||
 | 
					<svg version="1.0" id="Layer_2_00000035512755216235750980000003796724444709727912_"
 | 
				
			||||||
 | 
						 xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px" viewBox="0 0 223 223"
 | 
				
			||||||
 | 
						 style="enable-background:new 0 0 223 223;" xml:space="preserve">
 | 
				
			||||||
 | 
					<g id="Layer_1-2">
 | 
				
			||||||
 | 
						<circle style="fill:none;" cx="111.5" cy="111.5" r="111.5"/>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							<linearGradient id="SVGID_00000120554527348650398850000014100179473629740711_" gradientUnits="userSpaceOnUse" x1="95.9513" y1="704.4023" x2="167.8534" y2="776.3043" gradientTransform="matrix(1 0 0 1 0 -568)">
 | 
				
			||||||
 | 
							<stop  offset="0" style="stop-color:#FFFFFF;stop-opacity:0"/>
 | 
				
			||||||
 | 
							<stop  offset="0.14" style="stop-color:#FFFFFF;stop-opacity:0.37"/>
 | 
				
			||||||
 | 
							<stop  offset="1" style="stop-color:#FFFFFF"/>
 | 
				
			||||||
 | 
						</linearGradient>
 | 
				
			||||||
 | 
						<path style="fill:url(#SVGID_00000120554527348650398850000014100179473629740711_);" d="M192.2,141c-9.9-0.1-18,7.8-18.1,17.7c0,3.4,0.9,6.8,2.7,9.6c-4.5,5.2-9.5,10-15,14.1
 | 
				
			||||||
 | 
							c-10,7.6-21.5,13-33.8,15.7c-12.4,2.7-25.2,2.5-37.5-0.4c-11.4-2.6-23.2-8-34.9-15.9l-0.1-0.1c-2.2-1.3-5.1-0.6-6.4,1.6
 | 
				
			||||||
 | 
							c-1.2,2-0.8,4.6,1.1,6.1l0.1,0.1c12.5,9,25.1,15.3,37.6,18.7c8.1,2.3,16.5,3.4,24.9,3.5c5.9,0.1,11.8-0.4,17.7-1.5
 | 
				
			||||||
 | 
							c14.2-2.6,27.7-8.3,39.5-16.7c7-4.9,13.4-10.6,19.1-17.1c9.8,1.6,19-4.9,20.6-14.7c1.6-9.8-4.9-19-14.7-20.6
 | 
				
			||||||
 | 
							C194,141,193.1,141,192.2,141L192.2,141L192.2,141z"/>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							<linearGradient id="SVGID_00000120554527348650398850000014100179473629740724_" gradientUnits="userSpaceOnUse" x1="89.193" y1="666.3314" x2="9.5603" y2="666.3314" gradientTransform="matrix(1 0 0 1 0 -568)">
 | 
				
			||||||
 | 
							<stop  offset="0" style="stop-color:#FFFFFF;stop-opacity:0"/>
 | 
				
			||||||
 | 
							<stop  offset="0.14" style="stop-color:#FFFFFF;stop-opacity:0.37"/>
 | 
				
			||||||
 | 
							<stop  offset="1" style="stop-color:#FFFFFF"/>
 | 
				
			||||||
 | 
						</linearGradient>
 | 
				
			||||||
 | 
						<path style="fill:url(#SVGID_00000120554527348650398850000014100179473629740724_);" d="M51.8,158.5c-3.1-7.8-11.3-12.4-19.6-11
 | 
				
			||||||
 | 
							c-10.1-20.7-12.1-44.4-5.7-66.4c2.1-6.8,5-13.3,8.8-19.4c3.8-6.1,8.4-11.6,13.7-16.5c5.3-4.9,11.1-9.3,17.3-13
 | 
				
			||||||
 | 
							c6.4-3.7,13.1-6.9,20-9.5l0.6-0.2l0.5-0.4c1.6-1.3,2.2-3.4,1.5-5.3c-0.8-2.2-3.1-3.5-5.4-2.9l-0.4,0.1c-7.5,2.6-14.8,5.8-21.8,9.5
 | 
				
			||||||
 | 
							c-7,3.9-13.6,8.5-19.6,13.8c-6.1,5.4-11.5,11.5-16,18.2c-4.6,6.8-8.2,14.1-10.8,21.9c-5.2,15.5-6.6,32.1-4.1,48.3
 | 
				
			||||||
 | 
							c1.5,10.5,4.6,20.6,9.2,30.2c-5.2,8.4-2.6,19.4,5.8,24.6s19.4,2.6,24.6-5.8C53.3,169.7,53.9,163.7,51.8,158.5L51.8,158.5
 | 
				
			||||||
 | 
							L51.8,158.5z"/>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							<linearGradient id="SVGID_00000124152774908586921190000015569799832479483270_" gradientUnits="userSpaceOnUse" x1="152.6035" y1="707.0817" x2="152.6035" y2="568.2065" gradientTransform="matrix(1 0 0 1 0 -568)">
 | 
				
			||||||
 | 
							<stop  offset="0" style="stop-color:#FFFFFF;stop-opacity:0"/>
 | 
				
			||||||
 | 
							<stop  offset="0.14" style="stop-color:#FFFFFF;stop-opacity:0.37"/>
 | 
				
			||||||
 | 
							<stop  offset="1" style="stop-color:#FFFFFF"/>
 | 
				
			||||||
 | 
						</linearGradient>
 | 
				
			||||||
 | 
						<path style="fill:url(#SVGID_00000124152774908586921190000015569799832479483270_);" d="M107.5,35.4c8,2.2,16.5-1.4,20.4-8.7
 | 
				
			||||||
 | 
							C148.5,31,167.1,42,180.6,58c8.6,10.3,14.6,22.4,17.7,35.4c2.9,12.1,3.2,25.7,0.9,40.3l-0.1,0.5c-0.1,2.3,1.6,4.3,3.9,4.7
 | 
				
			||||||
 | 
							c0.2,0,0.4,0.1,0.7,0.1c1.8,0.1,3.5-1,4.3-2.6l0.2-0.5l0.1-0.5c3.1-15.9,3.3-30.8,0.5-44.3c-3-14.9-9.4-28.9-18.7-40.8
 | 
				
			||||||
 | 
							c-9.4-11.9-21.3-21.7-34.8-28.7c-8.3-4.3-17.2-7.5-26.4-9.6c-0.3-0.9-0.8-1.9-1.3-2.8c-4.9-8.6-15.8-11.6-24.4-6.7
 | 
				
			||||||
 | 
							S91.8,18.4,96.7,27C99,31.1,102.9,34.1,107.5,35.4L107.5,35.4L107.5,35.4z"/>
 | 
				
			||||||
 | 
					</g>
 | 
				
			||||||
 | 
					</svg>
 | 
				
			||||||
| 
		 After Width: | Height: | Size: 3.4 KiB  | 
							
								
								
									
										1
									
								
								src/assets/world-map.svg
									
									
									
									
									
										Normal file
									
								
							
							
						
						| 
		 After Width: | Height: | Size: 323 KiB  | 
							
								
								
									
										240
									
								
								src/common/SortedCollection.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						@ -0,0 +1,240 @@
 | 
				
			|||||||
 | 
					import { Maybe, Opaque } from './helpers';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export type Compare<T> = (a: T, b: T) => number;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export function sortedInsert<T>(
 | 
				
			||||||
 | 
					  item: T,
 | 
				
			||||||
 | 
					  into: Array<T>,
 | 
				
			||||||
 | 
					  compare: Compare<T>
 | 
				
			||||||
 | 
					): number {
 | 
				
			||||||
 | 
					  if (into.length === 0) {
 | 
				
			||||||
 | 
					    into.push(item);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    return 0;
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  let min = 0;
 | 
				
			||||||
 | 
					  let max = into.length - 1;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  while (min !== max) {
 | 
				
			||||||
 | 
					    const guess = ((min + max) / 2) | 0;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    if (compare(item, into[guess]) < 0) {
 | 
				
			||||||
 | 
					      max = Math.max(min, guess - 1);
 | 
				
			||||||
 | 
					    } else {
 | 
				
			||||||
 | 
					      min = Math.min(max, guess + 1);
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  const insert = compare(item, into[min]) <= 0 ? min : min + 1;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  into.splice(insert, 0, item);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  return insert;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export function sortedIndexOf<T>(
 | 
				
			||||||
 | 
					  item: T,
 | 
				
			||||||
 | 
					  within: Array<T>,
 | 
				
			||||||
 | 
					  compare: Compare<T>
 | 
				
			||||||
 | 
					): number {
 | 
				
			||||||
 | 
					  if (within.length === 0) {
 | 
				
			||||||
 | 
					    return -1;
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  let min = 0;
 | 
				
			||||||
 | 
					  let max = within.length - 1;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  while (min !== max) {
 | 
				
			||||||
 | 
					    const guess = ((min + max) / 2) | 0;
 | 
				
			||||||
 | 
					    const other = within[guess];
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    if (item === other) {
 | 
				
			||||||
 | 
					      return guess;
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    const result = compare(item, other);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    if (result < 0) {
 | 
				
			||||||
 | 
					      max = Math.max(min, guess - 1);
 | 
				
			||||||
 | 
					    } else if (result > 0) {
 | 
				
			||||||
 | 
					      min = Math.min(max, guess + 1);
 | 
				
			||||||
 | 
					    } else {
 | 
				
			||||||
 | 
					      // Equal sort value, but different reference, do value search from min
 | 
				
			||||||
 | 
					      return within.indexOf(item, min);
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  if (item === within[min]) {
 | 
				
			||||||
 | 
					    return min;
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  return -1;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					type StateRef = Opaque<number, 'SortedCollection.StateRef'>;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					interface Focus {
 | 
				
			||||||
 | 
					  start: number;
 | 
				
			||||||
 | 
					  end: number;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export class SortedCollection<Item extends { id: number }> {
 | 
				
			||||||
 | 
					  // Comparator function used to sort the collection
 | 
				
			||||||
 | 
					  private compare: Compare<Item>;
 | 
				
			||||||
 | 
					  // Mapping item `id` to the `Item`, this uses array as a structure with
 | 
				
			||||||
 | 
					  // the assumption that `id`s provided are increments from `0`, and that
 | 
				
			||||||
 | 
					  // vacant `id`s will be re-used in the future.
 | 
				
			||||||
 | 
					  private map = Array<Maybe<Item>>();
 | 
				
			||||||
 | 
					  // Actual sorted list of `Item`s.
 | 
				
			||||||
 | 
					  private list = Array<Item>();
 | 
				
			||||||
 | 
					  // Internal tracker for changes, this number increments whenever the
 | 
				
			||||||
 | 
					  // order of the **focused** elements in the collection changes
 | 
				
			||||||
 | 
					  private changeRef = 0;
 | 
				
			||||||
 | 
					  // Marks the range of indicies that are focused for tracking.
 | 
				
			||||||
 | 
					  // **Note:** `start` is inclusive, while `end` is exclusive (much like
 | 
				
			||||||
 | 
					  // `Array.slice()`).
 | 
				
			||||||
 | 
					  private focus: Focus = { start: 0, end: 0 };
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  constructor(compare: Compare<Item>) {
 | 
				
			||||||
 | 
					    this.compare = compare;
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  public setComparator(compare: Compare<Item>) {
 | 
				
			||||||
 | 
					    this.compare = compare;
 | 
				
			||||||
 | 
					    this.list = this.map.filter((item) => item != null) as Item[];
 | 
				
			||||||
 | 
					    this.list.sort(compare);
 | 
				
			||||||
 | 
					    this.changeRef += 1;
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  public add(item: Item) {
 | 
				
			||||||
 | 
					    if (this.map.length <= item.id) {
 | 
				
			||||||
 | 
					      // Grow map if item.id would be out of scope
 | 
				
			||||||
 | 
					      this.map = this.map.concat(
 | 
				
			||||||
 | 
					        Array<Maybe<Item>>(Math.max(10, 1 + item.id - this.map.length))
 | 
				
			||||||
 | 
					      );
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    // Remove old item if overriding
 | 
				
			||||||
 | 
					    this.remove(item.id);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    this.map[item.id] = item;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    const index = sortedInsert(item, this.list, this.compare);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    if (index < this.focus.end) {
 | 
				
			||||||
 | 
					      this.changeRef += 1;
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  public remove(id: number) {
 | 
				
			||||||
 | 
					    const item = this.map[id];
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    if (!item) {
 | 
				
			||||||
 | 
					      return;
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    const index = sortedIndexOf(item, this.list, this.compare);
 | 
				
			||||||
 | 
					    this.list.splice(index, 1);
 | 
				
			||||||
 | 
					    this.map[id] = null;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    if (index < this.focus.end) {
 | 
				
			||||||
 | 
					      this.changeRef += 1;
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  public get(id: number): Maybe<Item> {
 | 
				
			||||||
 | 
					    return this.map[id];
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  public sorted(): Array<Item> {
 | 
				
			||||||
 | 
					    return this.list;
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  public mut(id: number, mutator: (item: Item) => void) {
 | 
				
			||||||
 | 
					    const item = this.map[id];
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    if (!item) {
 | 
				
			||||||
 | 
					      return;
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    const index = sortedIndexOf(item, this.list, this.compare);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    mutator(item);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    if (index >= this.focus.start && index < this.focus.end) {
 | 
				
			||||||
 | 
					      this.changeRef += 1;
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  public mutAndSort(id: number, mutator: (item: Item) => void) {
 | 
				
			||||||
 | 
					    const item = this.map[id];
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    if (!item) {
 | 
				
			||||||
 | 
					      return;
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    const index = sortedIndexOf(item, this.list, this.compare);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    mutator(item);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    this.list.splice(index, 1);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    const newIndex = sortedInsert(item, this.list, this.compare);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    if (newIndex !== index) {
 | 
				
			||||||
 | 
					      const outOfFocus =
 | 
				
			||||||
 | 
					        (index < this.focus.start && newIndex < this.focus.start) ||
 | 
				
			||||||
 | 
					        (index >= this.focus.end && newIndex >= this.focus.end);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      if (!outOfFocus) {
 | 
				
			||||||
 | 
					        this.changeRef += 1;
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  public mutAndMaybeSort(
 | 
				
			||||||
 | 
					    id: number,
 | 
				
			||||||
 | 
					    mutator: (item: Item) => void,
 | 
				
			||||||
 | 
					    sort: boolean
 | 
				
			||||||
 | 
					  ) {
 | 
				
			||||||
 | 
					    if (sort) {
 | 
				
			||||||
 | 
					      this.mutAndSort(id, mutator);
 | 
				
			||||||
 | 
					    } else {
 | 
				
			||||||
 | 
					      this.mut(id, mutator);
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  public mutEach(mutator: (item: Item) => void) {
 | 
				
			||||||
 | 
					    this.list.forEach(mutator);
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  public mutEachAndSort(mutator: (item: Item) => void) {
 | 
				
			||||||
 | 
					    this.list.forEach(mutator);
 | 
				
			||||||
 | 
					    this.list.sort(this.compare);
 | 
				
			||||||
 | 
					    this.changeRef += 1;
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  public clear() {
 | 
				
			||||||
 | 
					    this.map = [];
 | 
				
			||||||
 | 
					    this.list = [];
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    this.changeRef += 1;
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  // Set a new `Focus`. Any changes to the order of items within the `Focus`
 | 
				
			||||||
 | 
					  // will increment `changeRef`.
 | 
				
			||||||
 | 
					  public setFocus(start: number, end: number) {
 | 
				
			||||||
 | 
					    this.focus = { start, end };
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  // Get the reference to current ordering state of focused items.
 | 
				
			||||||
 | 
					  public get ref(): StateRef {
 | 
				
			||||||
 | 
					    return this.changeRef as StateRef;
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  // Check if order of focused items has changed since obtaining a `ref`.
 | 
				
			||||||
 | 
					  public hasChangedSince(ref: StateRef): boolean {
 | 
				
			||||||
 | 
					    return this.changeRef > ref;
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
							
								
								
									
										83
									
								
								src/common/common.test.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						@ -0,0 +1,83 @@
 | 
				
			|||||||
 | 
					import { sortedInsert, sortedIndexOf } from '.';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					describe('sortedInsert', () => {
 | 
				
			||||||
 | 
					  it('inserts a value in the correct place', () => {
 | 
				
			||||||
 | 
					    function assertInsert(item: number, into: number[], equals: number[]) {
 | 
				
			||||||
 | 
					      const cmp = (a: number, b: number) => a - b;
 | 
				
			||||||
 | 
					      sortedInsert(item, into, cmp);
 | 
				
			||||||
 | 
					      expect(into).toStrictEqual(equals);
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    assertInsert(1, [2, 3, 4, 5, 6, 7, 8, 9], [1, 2, 3, 4, 5, 6, 7, 8, 9]);
 | 
				
			||||||
 | 
					    assertInsert(2, [1, 3, 4, 5, 6, 7, 8, 9], [1, 2, 3, 4, 5, 6, 7, 8, 9]);
 | 
				
			||||||
 | 
					    assertInsert(3, [1, 2, 4, 5, 6, 7, 8, 9], [1, 2, 3, 4, 5, 6, 7, 8, 9]);
 | 
				
			||||||
 | 
					    assertInsert(4, [1, 2, 3, 5, 6, 7, 8, 9], [1, 2, 3, 4, 5, 6, 7, 8, 9]);
 | 
				
			||||||
 | 
					    assertInsert(5, [1, 2, 3, 4, 6, 7, 8, 9], [1, 2, 3, 4, 5, 6, 7, 8, 9]);
 | 
				
			||||||
 | 
					    assertInsert(6, [1, 2, 3, 4, 5, 7, 8, 9], [1, 2, 3, 4, 5, 6, 7, 8, 9]);
 | 
				
			||||||
 | 
					    assertInsert(7, [1, 2, 3, 4, 5, 6, 8, 9], [1, 2, 3, 4, 5, 6, 7, 8, 9]);
 | 
				
			||||||
 | 
					    assertInsert(8, [1, 2, 3, 4, 5, 6, 7, 9], [1, 2, 3, 4, 5, 6, 7, 8, 9]);
 | 
				
			||||||
 | 
					    assertInsert(9, [1, 2, 3, 4, 5, 6, 7, 8], [1, 2, 3, 4, 5, 6, 7, 8, 9]);
 | 
				
			||||||
 | 
					  });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  it('fuzz tests insert as expected', () => {
 | 
				
			||||||
 | 
					    const cmp = (a: number, b: number) => a - b;
 | 
				
			||||||
 | 
					    const scramble = () => Math.random() - 0.5;
 | 
				
			||||||
 | 
					    const sorted = [1, 2, 3, 4, 5, 6, 7, 8, 9];
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    for (let i = 0; i < 50; i++) {
 | 
				
			||||||
 | 
					      const scrambled = sorted.sort(scramble);
 | 
				
			||||||
 | 
					      const resorted: number[] = [];
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      for (const item of scrambled) {
 | 
				
			||||||
 | 
					        sortedInsert(item, resorted, cmp);
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      expect(resorted).toStrictEqual([1, 2, 3, 4, 5, 6, 7, 8, 9]);
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					  });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  it('indexes', () => {
 | 
				
			||||||
 | 
					    const cmp = (a: number, b: number) => a - b;
 | 
				
			||||||
 | 
					    const into: number[] = [];
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    expect(sortedInsert(5, into, cmp)).toStrictEqual(0);
 | 
				
			||||||
 | 
					    expect(into).toStrictEqual([5]);
 | 
				
			||||||
 | 
					    expect(sortedInsert(1, into, cmp)).toStrictEqual(0);
 | 
				
			||||||
 | 
					    expect(into).toStrictEqual([1, 5]);
 | 
				
			||||||
 | 
					    expect(sortedInsert(9, into, cmp)).toStrictEqual(2);
 | 
				
			||||||
 | 
					    expect(into).toStrictEqual([1, 5, 9]);
 | 
				
			||||||
 | 
					    expect(sortedInsert(3, into, cmp)).toStrictEqual(1);
 | 
				
			||||||
 | 
					    expect(into).toStrictEqual([1, 3, 5, 9]);
 | 
				
			||||||
 | 
					    expect(sortedInsert(7, into, cmp)).toStrictEqual(3);
 | 
				
			||||||
 | 
					    expect(into).toStrictEqual([1, 3, 5, 7, 9]);
 | 
				
			||||||
 | 
					    expect(sortedInsert(4, into, cmp)).toStrictEqual(2);
 | 
				
			||||||
 | 
					    expect(into).toStrictEqual([1, 3, 4, 5, 7, 9]);
 | 
				
			||||||
 | 
					    expect(sortedInsert(6, into, cmp)).toStrictEqual(4);
 | 
				
			||||||
 | 
					    expect(into).toStrictEqual([1, 3, 4, 5, 6, 7, 9]);
 | 
				
			||||||
 | 
					    expect(sortedInsert(2, into, cmp)).toStrictEqual(1);
 | 
				
			||||||
 | 
					    expect(into).toStrictEqual([1, 2, 3, 4, 5, 6, 7, 9]);
 | 
				
			||||||
 | 
					    expect(sortedInsert(8, into, cmp)).toStrictEqual(7);
 | 
				
			||||||
 | 
					    expect(into).toStrictEqual([1, 2, 3, 4, 5, 6, 7, 8, 9]);
 | 
				
			||||||
 | 
					  });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  it('sortedIndexOf', () => {
 | 
				
			||||||
 | 
					    type ValueObj = {
 | 
				
			||||||
 | 
					      value: number;
 | 
				
			||||||
 | 
					    };
 | 
				
			||||||
 | 
					    const cmp = (a: ValueObj, b: ValueObj) => a.value - b.value;
 | 
				
			||||||
 | 
					    const array: Array<ValueObj> = [];
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    for (let i = 1; i <= 1000; i++) {
 | 
				
			||||||
 | 
					      array.push({ value: i >> 1 });
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    for (let i = 0; i < 50; i++) {
 | 
				
			||||||
 | 
					      const index = (Math.random() * 1000) | 0;
 | 
				
			||||||
 | 
					      const item = array[index];
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      expect(sortedIndexOf(item, array, cmp)).toStrictEqual(
 | 
				
			||||||
 | 
					        array.indexOf(item)
 | 
				
			||||||
 | 
					      );
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					  });
 | 
				
			||||||
 | 
					});
 | 
				
			||||||
							
								
								
									
										243
									
								
								src/common/feed.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						@ -0,0 +1,243 @@
 | 
				
			|||||||
 | 
					import { Maybe } from './helpers';
 | 
				
			||||||
 | 
					import { stringify, parse, Stringified } from './stringify';
 | 
				
			||||||
 | 
					import {
 | 
				
			||||||
 | 
					  FeedVersion,
 | 
				
			||||||
 | 
					  Address,
 | 
				
			||||||
 | 
					  Latitude,
 | 
				
			||||||
 | 
					  Longitude,
 | 
				
			||||||
 | 
					  City,
 | 
				
			||||||
 | 
					  NodeId,
 | 
				
			||||||
 | 
					  NodeCount,
 | 
				
			||||||
 | 
					  NodeDetails,
 | 
				
			||||||
 | 
					  NodeStats,
 | 
				
			||||||
 | 
					  NodeIO,
 | 
				
			||||||
 | 
					  NodeHardware,
 | 
				
			||||||
 | 
					  NodeLocation,
 | 
				
			||||||
 | 
					  BlockNumber,
 | 
				
			||||||
 | 
					  BlockHash,
 | 
				
			||||||
 | 
					  BlockDetails,
 | 
				
			||||||
 | 
					  Timestamp,
 | 
				
			||||||
 | 
					  Milliseconds,
 | 
				
			||||||
 | 
					  ChainLabel,
 | 
				
			||||||
 | 
					  GenesisHash,
 | 
				
			||||||
 | 
					  AuthoritySetInfo,
 | 
				
			||||||
 | 
					  ChainStats,
 | 
				
			||||||
 | 
					} from './types';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export const ACTIONS = {
 | 
				
			||||||
 | 
					  FeedVersion: 0x00 as const,
 | 
				
			||||||
 | 
					  BestBlock: 0x01 as const,
 | 
				
			||||||
 | 
					  BestFinalized: 0x02 as const,
 | 
				
			||||||
 | 
					  AddedNode: 0x03 as const,
 | 
				
			||||||
 | 
					  RemovedNode: 0x04 as const,
 | 
				
			||||||
 | 
					  LocatedNode: 0x05 as const,
 | 
				
			||||||
 | 
					  ImportedBlock: 0x06 as const,
 | 
				
			||||||
 | 
					  FinalizedBlock: 0x07 as const,
 | 
				
			||||||
 | 
					  NodeStats: 0x08 as const,
 | 
				
			||||||
 | 
					  NodeHardware: 0x09 as const,
 | 
				
			||||||
 | 
					  TimeSync: 0x0a as const,
 | 
				
			||||||
 | 
					  AddedChain: 0x0b as const,
 | 
				
			||||||
 | 
					  RemovedChain: 0x0c as const,
 | 
				
			||||||
 | 
					  SubscribedTo: 0x0d as const,
 | 
				
			||||||
 | 
					  UnsubscribedFrom: 0x0e as const,
 | 
				
			||||||
 | 
					  Pong: 0x0f as const,
 | 
				
			||||||
 | 
					  AfgFinalized: 0x10 as const,
 | 
				
			||||||
 | 
					  AfgReceivedPrevote: 0x11 as const,
 | 
				
			||||||
 | 
					  AfgReceivedPrecommit: 0x12 as const,
 | 
				
			||||||
 | 
					  AfgAuthoritySet: 0x13 as const,
 | 
				
			||||||
 | 
					  StaleNode: 0x14 as const,
 | 
				
			||||||
 | 
					  NodeIO: 0x15 as const,
 | 
				
			||||||
 | 
					  ChainStatsUpdate: 0x16 as const,
 | 
				
			||||||
 | 
					};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export type Action = typeof ACTIONS[keyof typeof ACTIONS];
 | 
				
			||||||
 | 
					export type Payload = Message['payload'];
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					interface MessageBase {
 | 
				
			||||||
 | 
					  action: Action;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					interface FeedVersionMessage extends MessageBase {
 | 
				
			||||||
 | 
					  action: typeof ACTIONS.FeedVersion;
 | 
				
			||||||
 | 
					  payload: FeedVersion;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					interface BestBlockMessage extends MessageBase {
 | 
				
			||||||
 | 
					  action: typeof ACTIONS.BestBlock;
 | 
				
			||||||
 | 
					  payload: [BlockNumber, Timestamp, Maybe<Milliseconds>];
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					interface BestFinalizedBlockMessage extends MessageBase {
 | 
				
			||||||
 | 
					  action: typeof ACTIONS.BestFinalized;
 | 
				
			||||||
 | 
					  payload: [BlockNumber, BlockHash];
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					interface AddedNodeMessage extends MessageBase {
 | 
				
			||||||
 | 
					  action: typeof ACTIONS.AddedNode;
 | 
				
			||||||
 | 
					  payload: [
 | 
				
			||||||
 | 
					    NodeId,
 | 
				
			||||||
 | 
					    NodeDetails,
 | 
				
			||||||
 | 
					    NodeStats,
 | 
				
			||||||
 | 
					    NodeIO,
 | 
				
			||||||
 | 
					    NodeHardware,
 | 
				
			||||||
 | 
					    BlockDetails,
 | 
				
			||||||
 | 
					    Maybe<NodeLocation>,
 | 
				
			||||||
 | 
					    Maybe<Timestamp>
 | 
				
			||||||
 | 
					  ];
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					interface RemovedNodeMessage extends MessageBase {
 | 
				
			||||||
 | 
					  action: typeof ACTIONS.RemovedNode;
 | 
				
			||||||
 | 
					  payload: NodeId;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					interface LocatedNodeMessage extends MessageBase {
 | 
				
			||||||
 | 
					  action: typeof ACTIONS.LocatedNode;
 | 
				
			||||||
 | 
					  payload: [NodeId, Latitude, Longitude, City];
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					interface ImportedBlockMessage extends MessageBase {
 | 
				
			||||||
 | 
					  action: typeof ACTIONS.ImportedBlock;
 | 
				
			||||||
 | 
					  payload: [NodeId, BlockDetails];
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					interface FinalizedBlockMessage extends MessageBase {
 | 
				
			||||||
 | 
					  action: typeof ACTIONS.FinalizedBlock;
 | 
				
			||||||
 | 
					  payload: [NodeId, BlockNumber, BlockHash];
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					interface NodeStatsMessage extends MessageBase {
 | 
				
			||||||
 | 
					  action: typeof ACTIONS.NodeStats;
 | 
				
			||||||
 | 
					  payload: [NodeId, NodeStats];
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					interface NodeHardwareMessage extends MessageBase {
 | 
				
			||||||
 | 
					  action: typeof ACTIONS.NodeHardware;
 | 
				
			||||||
 | 
					  payload: [NodeId, NodeHardware];
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					interface NodeIOMessage extends MessageBase {
 | 
				
			||||||
 | 
					  action: typeof ACTIONS.NodeIO;
 | 
				
			||||||
 | 
					  payload: [NodeId, NodeIO];
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					interface TimeSyncMessage extends MessageBase {
 | 
				
			||||||
 | 
					  action: typeof ACTIONS.TimeSync;
 | 
				
			||||||
 | 
					  payload: Timestamp;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					interface AddedChainMessage extends MessageBase {
 | 
				
			||||||
 | 
					  action: typeof ACTIONS.AddedChain;
 | 
				
			||||||
 | 
					  payload: [ChainLabel, GenesisHash, NodeCount];
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					interface RemovedChainMessage extends MessageBase {
 | 
				
			||||||
 | 
					  action: typeof ACTIONS.RemovedChain;
 | 
				
			||||||
 | 
					  payload: GenesisHash;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					interface SubscribedToMessage extends MessageBase {
 | 
				
			||||||
 | 
					  action: typeof ACTIONS.SubscribedTo;
 | 
				
			||||||
 | 
					  payload: GenesisHash;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					interface UnsubscribedFromMessage extends MessageBase {
 | 
				
			||||||
 | 
					  action: typeof ACTIONS.UnsubscribedFrom;
 | 
				
			||||||
 | 
					  payload: GenesisHash;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					interface PongMessage extends MessageBase {
 | 
				
			||||||
 | 
					  action: typeof ACTIONS.Pong;
 | 
				
			||||||
 | 
					  payload: string; // just echo whatever `ping` sent
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					interface AfgFinalizedMessage extends MessageBase {
 | 
				
			||||||
 | 
					  action: typeof ACTIONS.AfgFinalized;
 | 
				
			||||||
 | 
					  payload: [Address, BlockNumber, BlockHash];
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					interface AfgAuthoritySet extends MessageBase {
 | 
				
			||||||
 | 
					  action: typeof ACTIONS.AfgAuthoritySet;
 | 
				
			||||||
 | 
					  payload: AuthoritySetInfo;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					interface AfgReceivedPrecommit extends MessageBase {
 | 
				
			||||||
 | 
					  action: typeof ACTIONS.AfgReceivedPrecommit;
 | 
				
			||||||
 | 
					  payload: [Address, BlockNumber, BlockHash, Address];
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					interface AfgReceivedPrevote extends MessageBase {
 | 
				
			||||||
 | 
					  action: typeof ACTIONS.AfgReceivedPrevote;
 | 
				
			||||||
 | 
					  payload: [Address, BlockNumber, BlockHash, Address];
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					interface StaleNodeMessage extends MessageBase {
 | 
				
			||||||
 | 
					  action: typeof ACTIONS.StaleNode;
 | 
				
			||||||
 | 
					  payload: NodeId;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					interface ChainStatsUpdate extends MessageBase {
 | 
				
			||||||
 | 
					  action: typeof ACTIONS.ChainStatsUpdate;
 | 
				
			||||||
 | 
					  payload: ChainStats;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export type Message =
 | 
				
			||||||
 | 
					  | FeedVersionMessage
 | 
				
			||||||
 | 
					  | BestBlockMessage
 | 
				
			||||||
 | 
					  | BestFinalizedBlockMessage
 | 
				
			||||||
 | 
					  | AddedNodeMessage
 | 
				
			||||||
 | 
					  | RemovedNodeMessage
 | 
				
			||||||
 | 
					  | LocatedNodeMessage
 | 
				
			||||||
 | 
					  | ImportedBlockMessage
 | 
				
			||||||
 | 
					  | FinalizedBlockMessage
 | 
				
			||||||
 | 
					  | NodeStatsMessage
 | 
				
			||||||
 | 
					  | NodeHardwareMessage
 | 
				
			||||||
 | 
					  | TimeSyncMessage
 | 
				
			||||||
 | 
					  | AddedChainMessage
 | 
				
			||||||
 | 
					  | RemovedChainMessage
 | 
				
			||||||
 | 
					  | SubscribedToMessage
 | 
				
			||||||
 | 
					  | UnsubscribedFromMessage
 | 
				
			||||||
 | 
					  | AfgFinalizedMessage
 | 
				
			||||||
 | 
					  | AfgReceivedPrevote
 | 
				
			||||||
 | 
					  | AfgReceivedPrecommit
 | 
				
			||||||
 | 
					  | AfgAuthoritySet
 | 
				
			||||||
 | 
					  | StaleNodeMessage
 | 
				
			||||||
 | 
					  | PongMessage
 | 
				
			||||||
 | 
					  | NodeIOMessage
 | 
				
			||||||
 | 
					  | ChainStatsUpdate;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export type SquashedMessages = Array<Action | Payload>;
 | 
				
			||||||
 | 
					export type Data = Stringified<SquashedMessages>;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export function serialize(messages: Array<Message>): Data {
 | 
				
			||||||
 | 
					  const squashed: SquashedMessages = new Array(messages.length * 2);
 | 
				
			||||||
 | 
					  let index = 0;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  messages.forEach((message) => {
 | 
				
			||||||
 | 
					    const { action, payload } = message;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    squashed[index++] = action;
 | 
				
			||||||
 | 
					    squashed[index++] = payload;
 | 
				
			||||||
 | 
					  });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  return stringify(squashed);
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export function deserialize(data: Data): Array<Message> {
 | 
				
			||||||
 | 
					  const json = parse(data);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  if (!Array.isArray(json) || json.length === 0 || json.length % 2 !== 0) {
 | 
				
			||||||
 | 
					    throw new Error('Invalid FeedMessage.Data');
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  const messages = new Array<Message>(json.length / 2);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  for (const index of messages.keys()) {
 | 
				
			||||||
 | 
					    const [action, payload] = json.slice(index * 2);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    messages[index] = { action, payload } as Message;
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  return messages;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
							
								
								
									
										82
									
								
								src/common/helpers.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						@ -0,0 +1,82 @@
 | 
				
			|||||||
 | 
					import { Milliseconds, Timestamp } from './types';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export abstract class PhantomData<P> {
 | 
				
			||||||
 | 
					  public __PHANTOM__: P;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export type Opaque<T, P> = T & PhantomData<P>;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export type Maybe<T> = T | null | undefined;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export function sleep(time: Milliseconds): Promise<void> {
 | 
				
			||||||
 | 
					  return new Promise<void>((resolve) => {
 | 
				
			||||||
 | 
					    setTimeout(() => resolve(), time);
 | 
				
			||||||
 | 
					  });
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export const timestamp = Date.now as () => Timestamp;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export class NumStats<T extends number> {
 | 
				
			||||||
 | 
					  private readonly stack: Array<T>;
 | 
				
			||||||
 | 
					  private readonly history: number;
 | 
				
			||||||
 | 
					  private index = 0;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  constructor(history: number) {
 | 
				
			||||||
 | 
					    if (history < 1) {
 | 
				
			||||||
 | 
					      throw new Error('Must track at least one number');
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    this.history = history;
 | 
				
			||||||
 | 
					    this.stack = new Array(history);
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  public push(val: T) {
 | 
				
			||||||
 | 
					    this.stack[this.index++ % this.history] = val;
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  public average(): T {
 | 
				
			||||||
 | 
					    if (this.index === 0) {
 | 
				
			||||||
 | 
					      return 0 as T;
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    const list = this.nonEmpty();
 | 
				
			||||||
 | 
					    let sum = 0;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    for (const n of list as Array<number>) {
 | 
				
			||||||
 | 
					      sum += n;
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    return (sum / list.length) as T;
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  public averageWithoutExtremes(extremes: number): T {
 | 
				
			||||||
 | 
					    if (this.index === 0) {
 | 
				
			||||||
 | 
					      return 0 as T;
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    const list = this.nonEmpty();
 | 
				
			||||||
 | 
					    const count = list.length - extremes * 2;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    if (count < 1) {
 | 
				
			||||||
 | 
					      // Not enough entries to remove desired number of extremes,
 | 
				
			||||||
 | 
					      // fall back to regular average
 | 
				
			||||||
 | 
					      return this.average();
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    let sum = 0;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    for (const n of [...list]
 | 
				
			||||||
 | 
					      .sort((a, b) => a - b)
 | 
				
			||||||
 | 
					      .slice(extremes, -extremes)) {
 | 
				
			||||||
 | 
					      sum += n;
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    return (sum / count) as T;
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  private nonEmpty(): Readonly<Array<number>> {
 | 
				
			||||||
 | 
					    return this.index < this.history
 | 
				
			||||||
 | 
					      ? this.stack.slice(0, this.index)
 | 
				
			||||||
 | 
					      : this.stack;
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
							
								
								
									
										9
									
								
								src/common/id.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						@ -0,0 +1,9 @@
 | 
				
			|||||||
 | 
					import { Opaque } from './helpers';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export type Id<T> = Opaque<number, T>;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export function idGenerator<I extends Id<any>>(): () => I {
 | 
				
			||||||
 | 
					  let current = 0;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  return () => current++ as I;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
							
								
								
									
										11
									
								
								src/common/index.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						@ -0,0 +1,11 @@
 | 
				
			|||||||
 | 
					export * from './helpers';
 | 
				
			||||||
 | 
					export * from './id';
 | 
				
			||||||
 | 
					export * from './stringify';
 | 
				
			||||||
 | 
					export * from './SortedCollection';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					import * as Types from './types';
 | 
				
			||||||
 | 
					import * as FeedMessage from './feed';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export { Types, FeedMessage };
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export const VERSION: Types.FeedVersion = 32 as Types.FeedVersion;
 | 
				
			||||||
							
								
								
									
										84
									
								
								src/common/iterators.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						@ -0,0 +1,84 @@
 | 
				
			|||||||
 | 
					export function* map<T, U>(
 | 
				
			||||||
 | 
					  iter: IterableIterator<T>,
 | 
				
			||||||
 | 
					  fn: (item: T) => U
 | 
				
			||||||
 | 
					): IterableIterator<U> {
 | 
				
			||||||
 | 
					  for (const item of iter) {
 | 
				
			||||||
 | 
					    yield fn(item);
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export function* chain<T>(
 | 
				
			||||||
 | 
					  a: IterableIterator<T>,
 | 
				
			||||||
 | 
					  b: IterableIterator<T>
 | 
				
			||||||
 | 
					): IterableIterator<T> {
 | 
				
			||||||
 | 
					  yield* a;
 | 
				
			||||||
 | 
					  yield* b;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export function* zip<T, U>(
 | 
				
			||||||
 | 
					  a: IterableIterator<T>,
 | 
				
			||||||
 | 
					  b: IterableIterator<U>
 | 
				
			||||||
 | 
					): IterableIterator<[T, U]> {
 | 
				
			||||||
 | 
					  let itemA = a.next();
 | 
				
			||||||
 | 
					  let itemB = b.next();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  while (!itemA.done && !itemB.done) {
 | 
				
			||||||
 | 
					    yield [itemA.value, itemB.value];
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    itemA = a.next();
 | 
				
			||||||
 | 
					    itemB = b.next();
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export function* take<T>(
 | 
				
			||||||
 | 
					  iter: IterableIterator<T>,
 | 
				
			||||||
 | 
					  n: number
 | 
				
			||||||
 | 
					): IterableIterator<T> {
 | 
				
			||||||
 | 
					  for (const item of iter) {
 | 
				
			||||||
 | 
					    if (n-- === 0) {
 | 
				
			||||||
 | 
					      return;
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    yield item;
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export function skip<T>(
 | 
				
			||||||
 | 
					  iter: IterableIterator<T>,
 | 
				
			||||||
 | 
					  n: number
 | 
				
			||||||
 | 
					): IterableIterator<T> {
 | 
				
			||||||
 | 
					  while (n-- !== 0 && !iter.next().done) {}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  return iter;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export function reduce<T, R>(
 | 
				
			||||||
 | 
					  iter: IterableIterator<T>,
 | 
				
			||||||
 | 
					  fn: (accu: R, item: T) => R,
 | 
				
			||||||
 | 
					  accumulator: R
 | 
				
			||||||
 | 
					): R {
 | 
				
			||||||
 | 
					  for (const item of iter) {
 | 
				
			||||||
 | 
					    accumulator = fn(accumulator, item);
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  return accumulator;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export function join(
 | 
				
			||||||
 | 
					  iter: IterableIterator<{ toString: () => string }>,
 | 
				
			||||||
 | 
					  glue: string
 | 
				
			||||||
 | 
					): string {
 | 
				
			||||||
 | 
					  const first = iter.next();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  if (first.done) {
 | 
				
			||||||
 | 
					    return '';
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  let result = first.value.toString();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  for (const item of iter) {
 | 
				
			||||||
 | 
					    result += glue + item;
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  return result;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
							
								
								
									
										8
									
								
								src/common/stringify.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						@ -0,0 +1,8 @@
 | 
				
			|||||||
 | 
					export abstract class Stringified<T> {
 | 
				
			||||||
 | 
					  public __PHANTOM__: T;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// eslint-disable-next-line @typescript-eslint/no-explicit-any
 | 
				
			||||||
 | 
					export const parse = JSON.parse as any as <T>(val: Stringified<T>) => T;
 | 
				
			||||||
 | 
					// eslint-disable-next-line @typescript-eslint/no-explicit-any
 | 
				
			||||||
 | 
					export const stringify = JSON.stringify as any as <T>(val: T) => Stringified<T>;
 | 
				
			||||||
							
								
								
									
										122
									
								
								src/common/types.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						@ -0,0 +1,122 @@
 | 
				
			|||||||
 | 
					import { Opaque, Maybe } from './helpers';
 | 
				
			||||||
 | 
					import { Id } from './id';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export type FeedVersion = Opaque<number, 'FeedVersion'>;
 | 
				
			||||||
 | 
					export type ChainLabel = Opaque<string, 'ChainLabel'>;
 | 
				
			||||||
 | 
					export type GenesisHash = Opaque<string, 'GenesisHash'>;
 | 
				
			||||||
 | 
					export type FeedId = Id<'Feed'>;
 | 
				
			||||||
 | 
					export type NodeId = Id<'Node'>;
 | 
				
			||||||
 | 
					export type NodeName = Opaque<string, 'NodeName'>;
 | 
				
			||||||
 | 
					export type NodeImplementation = Opaque<string, 'NodeImplementation'>;
 | 
				
			||||||
 | 
					export type NodeVersion = Opaque<string, 'NodeVersion'>;
 | 
				
			||||||
 | 
					export type OperatingSystem = Opaque<string, 'OperatingSystem'>;
 | 
				
			||||||
 | 
					export type CpuArchitecture = Opaque<string, 'CpuArchitecture'>;
 | 
				
			||||||
 | 
					export type Cpu = string;
 | 
				
			||||||
 | 
					export type CpuCores = number;
 | 
				
			||||||
 | 
					export type TargetEnv = string;
 | 
				
			||||||
 | 
					export type Memory = number;
 | 
				
			||||||
 | 
					export type VirtualMachine = boolean;
 | 
				
			||||||
 | 
					export type LinuxKernel = string;
 | 
				
			||||||
 | 
					export type LinuxDistro = string;
 | 
				
			||||||
 | 
					export type BlockNumber = Opaque<number, 'BlockNumber'>;
 | 
				
			||||||
 | 
					export type BlockHash = Opaque<string, 'BlockHash'>;
 | 
				
			||||||
 | 
					export type Address = Opaque<string, 'Address'>;
 | 
				
			||||||
 | 
					export type Milliseconds = Opaque<number, 'Milliseconds'>;
 | 
				
			||||||
 | 
					export type Timestamp = Opaque<Milliseconds, 'Timestamp'>;
 | 
				
			||||||
 | 
					export type PropagationTime = Opaque<Milliseconds, 'PropagationTime'>;
 | 
				
			||||||
 | 
					export type NodeCount = Opaque<number, 'NodeCount'>;
 | 
				
			||||||
 | 
					export type PeerCount = Opaque<number, 'PeerCount'>;
 | 
				
			||||||
 | 
					export type TransactionCount = Opaque<number, 'TransactionCount'>;
 | 
				
			||||||
 | 
					export type Latitude = Opaque<number, 'Latitude'>;
 | 
				
			||||||
 | 
					export type Longitude = Opaque<number, 'Longitude'>;
 | 
				
			||||||
 | 
					export type City = Opaque<string, 'City'>;
 | 
				
			||||||
 | 
					export type MemoryUse = Opaque<number, 'MemoryUse'>;
 | 
				
			||||||
 | 
					export type CPUUse = Opaque<number, 'CPUUse'>;
 | 
				
			||||||
 | 
					export type Bytes = Opaque<number, 'Bytes'>;
 | 
				
			||||||
 | 
					export type BytesPerSecond = Opaque<number, 'BytesPerSecond'>;
 | 
				
			||||||
 | 
					export type NetworkId = Opaque<string, 'NetworkId'>;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export type NodeSysInfo = {
 | 
				
			||||||
 | 
					  cpu: string;
 | 
				
			||||||
 | 
					  memory: number;
 | 
				
			||||||
 | 
					  core_count: number;
 | 
				
			||||||
 | 
					  linux_kernel: string;
 | 
				
			||||||
 | 
					  linux_distro: string;
 | 
				
			||||||
 | 
					  is_virtual_machine: boolean;
 | 
				
			||||||
 | 
					};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export type BlockDetails = [
 | 
				
			||||||
 | 
					  BlockNumber,
 | 
				
			||||||
 | 
					  BlockHash,
 | 
				
			||||||
 | 
					  Milliseconds,
 | 
				
			||||||
 | 
					  Timestamp,
 | 
				
			||||||
 | 
					  Maybe<PropagationTime>
 | 
				
			||||||
 | 
					];
 | 
				
			||||||
 | 
					export type NodeDetails = [
 | 
				
			||||||
 | 
					  NodeName,
 | 
				
			||||||
 | 
					  NodeImplementation,
 | 
				
			||||||
 | 
					  NodeVersion,
 | 
				
			||||||
 | 
					  Maybe<Address>,
 | 
				
			||||||
 | 
					  Maybe<NetworkId>,
 | 
				
			||||||
 | 
					  OperatingSystem,
 | 
				
			||||||
 | 
					  CpuArchitecture,
 | 
				
			||||||
 | 
					  TargetEnv,
 | 
				
			||||||
 | 
					  undefined,
 | 
				
			||||||
 | 
					  NodeSysInfo
 | 
				
			||||||
 | 
					];
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export type NodeStats = [PeerCount, TransactionCount];
 | 
				
			||||||
 | 
					export type NodeIO = [Array<Bytes>];
 | 
				
			||||||
 | 
					export type NodeHardware = [
 | 
				
			||||||
 | 
					  Array<BytesPerSecond>,
 | 
				
			||||||
 | 
					  Array<BytesPerSecond>,
 | 
				
			||||||
 | 
					  Array<Timestamp>
 | 
				
			||||||
 | 
					];
 | 
				
			||||||
 | 
					export type NodeLocation = [Latitude, Longitude, City];
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export interface Authority {
 | 
				
			||||||
 | 
					  Address: Address;
 | 
				
			||||||
 | 
					  NodeId: Maybe<NodeId>;
 | 
				
			||||||
 | 
					  Name: Maybe<NodeName>;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					export declare type Authorities = Array<Address>;
 | 
				
			||||||
 | 
					export declare type AuthoritySetId = Opaque<number, 'AuthoritySetId'>;
 | 
				
			||||||
 | 
					export declare type AuthoritySetInfo = [
 | 
				
			||||||
 | 
					  AuthoritySetId,
 | 
				
			||||||
 | 
					  Authorities,
 | 
				
			||||||
 | 
					  Address,
 | 
				
			||||||
 | 
					  BlockNumber,
 | 
				
			||||||
 | 
					  BlockHash
 | 
				
			||||||
 | 
					];
 | 
				
			||||||
 | 
					export declare type Precommit = Opaque<boolean, 'Precommit'>;
 | 
				
			||||||
 | 
					export declare type Prevote = Opaque<boolean, 'Prevote'>;
 | 
				
			||||||
 | 
					export declare type Finalized = Opaque<boolean, 'Finalized'>;
 | 
				
			||||||
 | 
					export declare type ImplicitPrecommit = Opaque<boolean, 'ImplicitPrecommit'>;
 | 
				
			||||||
 | 
					export declare type ImplicitPrevote = Opaque<boolean, 'ImplicitPrevote'>;
 | 
				
			||||||
 | 
					export declare type ImplicitFinalized = Opaque<boolean, 'ImplicitFinalized'>;
 | 
				
			||||||
 | 
					export declare type ImplicitPointer = Opaque<BlockNumber, 'ImplicitPointer'>;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export type Ranking<T> = {
 | 
				
			||||||
 | 
					  list: Array<[T, number]>;
 | 
				
			||||||
 | 
					  other: number;
 | 
				
			||||||
 | 
					  unknown: number;
 | 
				
			||||||
 | 
					};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export type Range = [number, number | null];
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export type ChainStats = {
 | 
				
			||||||
 | 
					  version: Maybe<Ranking<string>>;
 | 
				
			||||||
 | 
					  target_os: Maybe<Ranking<string>>;
 | 
				
			||||||
 | 
					  target_arch: Maybe<Ranking<string>>;
 | 
				
			||||||
 | 
					  cpu: Maybe<Ranking<string>>;
 | 
				
			||||||
 | 
					  core_count: Maybe<Ranking<number>>;
 | 
				
			||||||
 | 
					  memory: Maybe<Ranking<Range>>;
 | 
				
			||||||
 | 
					  is_virtual_machine: Maybe<Ranking<boolean>>;
 | 
				
			||||||
 | 
					  linux_distro: Maybe<Ranking<string>>;
 | 
				
			||||||
 | 
					  linux_kernel: Maybe<Ranking<string>>;
 | 
				
			||||||
 | 
					  cpu_hashrate_score: Maybe<Ranking<Range>>;
 | 
				
			||||||
 | 
					  memory_memcpy_score: Maybe<Ranking<Range>>;
 | 
				
			||||||
 | 
					  disk_sequential_write_score: Maybe<Ranking<Range>>;
 | 
				
			||||||
 | 
					  disk_random_write_score: Maybe<Ranking<Range>>;
 | 
				
			||||||
 | 
					  cpu_vendor: Maybe<Ranking<string>>;
 | 
				
			||||||
 | 
					};
 | 
				
			||||||
							
								
								
									
										100
									
								
								src/components/Ago.tsx
									
									
									
									
									
										Normal file
									
								
							
							
						
						@ -0,0 +1,100 @@
 | 
				
			|||||||
 | 
					import * as React from 'react';
 | 
				
			||||||
 | 
					import './Tile.css';
 | 
				
			||||||
 | 
					import { timestamp, Types } from '../common';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					interface AgoProps {
 | 
				
			||||||
 | 
					  when: Types.Timestamp;
 | 
				
			||||||
 | 
					  justTime?: boolean;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					interface AgoState {
 | 
				
			||||||
 | 
					  now: Types.Timestamp;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					const tickers = new Map<Ago, (ts: Types.Timestamp) => void>();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					function tick() {
 | 
				
			||||||
 | 
					  const now = timestamp();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  for (const ticker of tickers.values()) {
 | 
				
			||||||
 | 
					    ticker(now);
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  setTimeout(tick, 100);
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					tick();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export class Ago extends React.Component<AgoProps, AgoState> {
 | 
				
			||||||
 | 
					  public static timeDiff = 0 as Types.Milliseconds;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  public state: AgoState;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  private agoStr: string;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  constructor(props: AgoProps) {
 | 
				
			||||||
 | 
					    super(props);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    this.state = {
 | 
				
			||||||
 | 
					      now: (timestamp() - Ago.timeDiff) as Types.Timestamp,
 | 
				
			||||||
 | 
					    };
 | 
				
			||||||
 | 
					    this.agoStr = this.stringify(props.when, this.state.now);
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  public shouldComponentUpdate(nextProps: AgoProps, nextState: AgoState) {
 | 
				
			||||||
 | 
					    const nextAgoStr = this.stringify(nextProps.when, nextState.now);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    if (this.agoStr !== nextAgoStr) {
 | 
				
			||||||
 | 
					      this.agoStr = nextAgoStr;
 | 
				
			||||||
 | 
					      return true;
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    return false;
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  public componentDidMount() {
 | 
				
			||||||
 | 
					    tickers.set(this, (now) => {
 | 
				
			||||||
 | 
					      this.setState({
 | 
				
			||||||
 | 
					        now: (now - Ago.timeDiff) as Types.Timestamp,
 | 
				
			||||||
 | 
					      });
 | 
				
			||||||
 | 
					    });
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  public componentWillUnmount() {
 | 
				
			||||||
 | 
					    tickers.delete(this);
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  public render() {
 | 
				
			||||||
 | 
					    if (this.props.when === 0) {
 | 
				
			||||||
 | 
					      return <span>-</span>;
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    return (
 | 
				
			||||||
 | 
					      <span title={new Date(this.props.when).toUTCString()}>{this.agoStr}</span>
 | 
				
			||||||
 | 
					    );
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  private stringify(when: number, now: number): string {
 | 
				
			||||||
 | 
					    const ago = Math.max(now - when, 0) / 1000;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    let agoStr: string;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    if (ago < 10) {
 | 
				
			||||||
 | 
					      agoStr = `${ago.toFixed(1)}s`;
 | 
				
			||||||
 | 
					    } else if (ago < 60) {
 | 
				
			||||||
 | 
					      agoStr = `${ago | 0}s`;
 | 
				
			||||||
 | 
					    } else if (ago < 3600) {
 | 
				
			||||||
 | 
					      agoStr = `${(ago / 60) | 0}m`;
 | 
				
			||||||
 | 
					    } else if (ago < 3600 * 24) {
 | 
				
			||||||
 | 
					      agoStr = `${(ago / 3600) | 0}h`;
 | 
				
			||||||
 | 
					    } else {
 | 
				
			||||||
 | 
					      agoStr = `${(ago / (3600 * 24)) | 0}d`;
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    if (this.props.justTime !== true) {
 | 
				
			||||||
 | 
					      agoStr += ' ago';
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    return agoStr;
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
							
								
								
									
										34
									
								
								src/components/Chain/Chain.css
									
									
									
									
									
										Normal file
									
								
							
							
						
						@ -0,0 +1,34 @@
 | 
				
			|||||||
 | 
					.Chain-content-container {
 | 
				
			||||||
 | 
					  position: absolute;
 | 
				
			||||||
 | 
					  left: 0;
 | 
				
			||||||
 | 
					  right: 0;
 | 
				
			||||||
 | 
					  bottom: 0;
 | 
				
			||||||
 | 
					  top: 148px;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					.Chain-content {
 | 
				
			||||||
 | 
					  width: 100%;
 | 
				
			||||||
 | 
					  min-width: 1350px;
 | 
				
			||||||
 | 
					  min-height: 100%;
 | 
				
			||||||
 | 
					  background: linear-gradient(90deg, rgba(28,54,100,1) 0%, rgba(45,87,132,1) 100%);
 | 
				
			||||||
 | 
					  color: #fff;
 | 
				
			||||||
 | 
					  box-shadow: rgba(0, 0, 0, 0.5) 0 3px 30px;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					.Chain-line-separator {
 | 
				
			||||||
 | 
					  width: 100%;
 | 
				
			||||||
 | 
					  min-width: 1350px;
 | 
				
			||||||
 | 
					  height: 40px;
 | 
				
			||||||
 | 
					  background: #f2e370;
 | 
				
			||||||
 | 
					  color: #000;
 | 
				
			||||||
 | 
					  font-size: 15px;
 | 
				
			||||||
 | 
					  font-weight: 600;
 | 
				
			||||||
 | 
					  display: flex;
 | 
				
			||||||
 | 
					  justify-content: center;
 | 
				
			||||||
 | 
					  align-items: center;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					.Chain-line-separator > a {
 | 
				
			||||||
 | 
					    color: #1f4671;
 | 
				
			||||||
 | 
					    cursor: pointer;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
							
								
								
									
										110
									
								
								src/components/Chain/Chain.tsx
									
									
									
									
									
										Normal file
									
								
							
							
						
						@ -0,0 +1,110 @@
 | 
				
			|||||||
 | 
					import * as React from 'react';
 | 
				
			||||||
 | 
					import { Connection } from '../../Connection';
 | 
				
			||||||
 | 
					import { Types, Maybe } from '../../common';
 | 
				
			||||||
 | 
					import {
 | 
				
			||||||
 | 
					  State as AppState,
 | 
				
			||||||
 | 
					  Update as AppUpdate,
 | 
				
			||||||
 | 
					  StateSettings,
 | 
				
			||||||
 | 
					} from '../../state';
 | 
				
			||||||
 | 
					import { getHashData } from '../../utils';
 | 
				
			||||||
 | 
					import { Header } from './';
 | 
				
			||||||
 | 
					import { List, WorldMap, Stats } from '../';
 | 
				
			||||||
 | 
					import { Persistent, PersistentObject, PersistentSet } from '../../persist';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					import './Chain.css';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export type ChainDisplay = 'list' | 'map' | 'settings' | 'consensus' | 'stats';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					interface ChainProps {
 | 
				
			||||||
 | 
					  appState: Readonly<AppState>;
 | 
				
			||||||
 | 
					  appUpdate: AppUpdate;
 | 
				
			||||||
 | 
					  connection: Promise<Connection>;
 | 
				
			||||||
 | 
					  settings: PersistentObject<StateSettings>;
 | 
				
			||||||
 | 
					  pins: PersistentSet<Types.NodeName>;
 | 
				
			||||||
 | 
					  sortBy: Persistent<Maybe<number>>;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					interface ChainState {
 | 
				
			||||||
 | 
					  display: ChainDisplay;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export class Chain extends React.Component<ChainProps, ChainState> {
 | 
				
			||||||
 | 
					  constructor(props: ChainProps) {
 | 
				
			||||||
 | 
					    super(props);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    let display: ChainDisplay = 'map';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    switch (getHashData().tab) {
 | 
				
			||||||
 | 
					      case 'list':
 | 
				
			||||||
 | 
					        display = 'list';
 | 
				
			||||||
 | 
					        break;
 | 
				
			||||||
 | 
					      case 'settings':
 | 
				
			||||||
 | 
					        display = 'settings';
 | 
				
			||||||
 | 
					        break;
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    this.state = {
 | 
				
			||||||
 | 
					      display,
 | 
				
			||||||
 | 
					    };
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  public render() {
 | 
				
			||||||
 | 
					    const { appState } = this.props;
 | 
				
			||||||
 | 
					    const { best, finalized, blockTimestamp, blockAverage, nodes } = appState;
 | 
				
			||||||
 | 
					    const { display: currentTab } = this.state;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    return (
 | 
				
			||||||
 | 
					      <div className="Chain">
 | 
				
			||||||
 | 
					        <Header
 | 
				
			||||||
 | 
					          nodesLength={nodes.sorted().length}
 | 
				
			||||||
 | 
					          best={best}
 | 
				
			||||||
 | 
					          finalized={finalized}
 | 
				
			||||||
 | 
					          blockAverage={blockAverage}
 | 
				
			||||||
 | 
					          blockTimestamp={blockTimestamp}
 | 
				
			||||||
 | 
					          currentTab={currentTab}
 | 
				
			||||||
 | 
					          setDisplay={this.setDisplay}
 | 
				
			||||||
 | 
					        />
 | 
				
			||||||
 | 
					        <div className="Chain-content-container">
 | 
				
			||||||
 | 
					          <div className="Chain-content">{this.renderContent()}</div>
 | 
				
			||||||
 | 
					        </div>
 | 
				
			||||||
 | 
					        <div className="Chain-line-separator">
 | 
				
			||||||
 | 
					          GHOST TestNet is Now Live. Check out the 
 | 
				
			||||||
 | 
					          <a onClick={()=> window.open("https://blog.ghostchain.io/ghost-chain-startup-guide/", "_blank")}>
 | 
				
			||||||
 | 
					            Quick Startup Guide
 | 
				
			||||||
 | 
					          </a>.
 | 
				
			||||||
 | 
					        </div>
 | 
				
			||||||
 | 
					      </div>
 | 
				
			||||||
 | 
					    );
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  private renderContent() {
 | 
				
			||||||
 | 
					    const { display } = this.state;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    const { appState, appUpdate, pins, sortBy } = this.props;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    if (display === 'list') {
 | 
				
			||||||
 | 
					      return (
 | 
				
			||||||
 | 
					        <List
 | 
				
			||||||
 | 
					          appState={appState}
 | 
				
			||||||
 | 
					          appUpdate={appUpdate}
 | 
				
			||||||
 | 
					          pins={pins}
 | 
				
			||||||
 | 
					          sortBy={sortBy}
 | 
				
			||||||
 | 
					        />
 | 
				
			||||||
 | 
					      );
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    if (display === 'map') {
 | 
				
			||||||
 | 
					      return <WorldMap appState={appState} />;
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    if (display === 'stats') {
 | 
				
			||||||
 | 
					      return <Stats appState={appState} />;
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    throw new Error('invalid `display`: ${display}');
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  private setDisplay = (display: ChainDisplay) => {
 | 
				
			||||||
 | 
					    this.setState({ display });
 | 
				
			||||||
 | 
					  };
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
							
								
								
									
										42
									
								
								src/components/Chain/Header.css
									
									
									
									
									
										Normal file
									
								
							
							
						
						@ -0,0 +1,42 @@
 | 
				
			|||||||
 | 
					.Header {
 | 
				
			||||||
 | 
					  background: linear-gradient(90deg, rgba(28,54,100,1) 0%, rgba(45,87,132,1) 100%);
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					.Header-container {
 | 
				
			||||||
 | 
					  margin: 0 5% 0;
 | 
				
			||||||
 | 
					  min-width: 1222px;
 | 
				
			||||||
 | 
					  width: 90%;
 | 
				
			||||||
 | 
					  height: 108px;
 | 
				
			||||||
 | 
					  display: flex;
 | 
				
			||||||
 | 
					  justify-content: space-between;
 | 
				
			||||||
 | 
					  align-items: center;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					.Header-container > *:hover {
 | 
				
			||||||
 | 
					  color: #f2e370;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					.Header-logo {
 | 
				
			||||||
 | 
					  width: 150px;
 | 
				
			||||||
 | 
					  height: 100%;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					.Header-logo > svg {
 | 
				
			||||||
 | 
					  width: 100%;
 | 
				
			||||||
 | 
					  height: 100%;
 | 
				
			||||||
 | 
					  color: #fff;
 | 
				
			||||||
 | 
					  cursor: pointer;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					.Header-logo > svg:hover {
 | 
				
			||||||
 | 
					  color: #f2e370;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					.Header-tabs {
 | 
				
			||||||
 | 
					  height: 95%;
 | 
				
			||||||
 | 
					  text-align: right;
 | 
				
			||||||
 | 
					  display: flex;
 | 
				
			||||||
 | 
					  flex-direction: column;
 | 
				
			||||||
 | 
					  justify-content: space-around;
 | 
				
			||||||
 | 
					  align-items: center;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
							
								
								
									
										100
									
								
								src/components/Chain/Header.tsx
									
									
									
									
									
										Normal file
									
								
							
							
						
						@ -0,0 +1,100 @@
 | 
				
			|||||||
 | 
					import * as React from 'react';
 | 
				
			||||||
 | 
					import { Types, Maybe } from '../../common';
 | 
				
			||||||
 | 
					import { formatNumber, secondsWithPrecision } from '../../utils';
 | 
				
			||||||
 | 
					import { Tab, ChainDisplay } from './';
 | 
				
			||||||
 | 
					import { Tile, Ago, Icon } from '../';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					import blockIcon from '../../icons/cube.svg';
 | 
				
			||||||
 | 
					import finalizedIcon from '../../icons/cube-alt.svg';
 | 
				
			||||||
 | 
					import blockTimeIcon from '../../icons/history.svg';
 | 
				
			||||||
 | 
					import lastTimeIcon from '../../icons/watch.svg';
 | 
				
			||||||
 | 
					import listIcon from '../../icons/list-alt-regular.svg';
 | 
				
			||||||
 | 
					import worldIcon from '../../icons/location.svg';
 | 
				
			||||||
 | 
					import settingsIcon from '../../icons/settings.svg';
 | 
				
			||||||
 | 
					import statsIcon from '../../icons/graph.svg';
 | 
				
			||||||
 | 
					import ghostLogoIcon from '../../icons/ghost-logo.svg';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					import './Header.css';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					interface HeaderProps {
 | 
				
			||||||
 | 
					  nodesLength: number;
 | 
				
			||||||
 | 
					  best: Types.BlockNumber;
 | 
				
			||||||
 | 
					  finalized: Types.BlockNumber;
 | 
				
			||||||
 | 
					  blockTimestamp: Types.Timestamp;
 | 
				
			||||||
 | 
					  blockAverage: Maybe<Types.Milliseconds>;
 | 
				
			||||||
 | 
					  currentTab: ChainDisplay;
 | 
				
			||||||
 | 
					  setDisplay: (display: ChainDisplay) => void;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export class Header extends React.Component<HeaderProps> {
 | 
				
			||||||
 | 
					  public shouldComponentUpdate(nextProps: HeaderProps) {
 | 
				
			||||||
 | 
					    return (
 | 
				
			||||||
 | 
					      this.props.nodesLength !== nextProps.nodesLength ||
 | 
				
			||||||
 | 
					      this.props.best !== nextProps.best ||
 | 
				
			||||||
 | 
					      this.props.finalized !== nextProps.finalized ||
 | 
				
			||||||
 | 
					      this.props.blockTimestamp !== nextProps.blockTimestamp ||
 | 
				
			||||||
 | 
					      this.props.blockAverage !== nextProps.blockAverage ||
 | 
				
			||||||
 | 
					      this.props.currentTab !== nextProps.currentTab
 | 
				
			||||||
 | 
					    );
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  public render() {
 | 
				
			||||||
 | 
					    const { nodesLength, best, finalized, blockTimestamp, blockAverage } = this.props;
 | 
				
			||||||
 | 
					    const { currentTab, setDisplay } = this.props;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    return (
 | 
				
			||||||
 | 
					      <div className="Header">
 | 
				
			||||||
 | 
					        <div className="Header-container">
 | 
				
			||||||
 | 
					          <div className="Header-logo" onClick={()=> window.open("https://ghostchain.io/", "_blank")}>
 | 
				
			||||||
 | 
					            <Icon src={ghostLogoIcon} />
 | 
				
			||||||
 | 
					          </div>
 | 
				
			||||||
 | 
					          <div className="Header-data">
 | 
				
			||||||
 | 
					            <Tile title="Latest Block">
 | 
				
			||||||
 | 
					              #{formatNumber(best)}
 | 
				
			||||||
 | 
					            </Tile>
 | 
				
			||||||
 | 
					            <Tile title="Finalized Block">
 | 
				
			||||||
 | 
					              #{formatNumber(finalized)}
 | 
				
			||||||
 | 
					            </Tile>
 | 
				
			||||||
 | 
					            <Tile title="Average Time">
 | 
				
			||||||
 | 
					              {blockAverage == null
 | 
				
			||||||
 | 
					                ? '-'
 | 
				
			||||||
 | 
					                : secondsWithPrecision(blockAverage / 1000)}
 | 
				
			||||||
 | 
					            </Tile>
 | 
				
			||||||
 | 
					            <Tile title="Last Block">
 | 
				
			||||||
 | 
					              <Ago when={blockTimestamp} />
 | 
				
			||||||
 | 
					            </Tile>
 | 
				
			||||||
 | 
					            <Tile title="Nodes">
 | 
				
			||||||
 | 
					              {nodesLength}
 | 
				
			||||||
 | 
					            </Tile>
 | 
				
			||||||
 | 
					          </div>
 | 
				
			||||||
 | 
					          <div className="Header-tabs">
 | 
				
			||||||
 | 
					            <Tab
 | 
				
			||||||
 | 
					              icon={worldIcon}
 | 
				
			||||||
 | 
					              label="Map"
 | 
				
			||||||
 | 
					              display="map"
 | 
				
			||||||
 | 
					              tab="map"
 | 
				
			||||||
 | 
					              current={currentTab}
 | 
				
			||||||
 | 
					              setDisplay={setDisplay}
 | 
				
			||||||
 | 
					            />
 | 
				
			||||||
 | 
					            <Tab
 | 
				
			||||||
 | 
					              icon={listIcon}
 | 
				
			||||||
 | 
					              label="List"
 | 
				
			||||||
 | 
					              display="list"
 | 
				
			||||||
 | 
					              tab=""
 | 
				
			||||||
 | 
					              current={currentTab}
 | 
				
			||||||
 | 
					              setDisplay={setDisplay}
 | 
				
			||||||
 | 
					            />
 | 
				
			||||||
 | 
					            <Tab
 | 
				
			||||||
 | 
					              icon={statsIcon}
 | 
				
			||||||
 | 
					              label="Stats"
 | 
				
			||||||
 | 
					              display="stats"
 | 
				
			||||||
 | 
					              tab="stats"
 | 
				
			||||||
 | 
					              current={currentTab}
 | 
				
			||||||
 | 
					              setDisplay={setDisplay}
 | 
				
			||||||
 | 
					            />
 | 
				
			||||||
 | 
					          </div>
 | 
				
			||||||
 | 
					        </div>
 | 
				
			||||||
 | 
					      </div>
 | 
				
			||||||
 | 
					    );
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
							
								
								
									
										24
									
								
								src/components/Chain/Tab.css
									
									
									
									
									
										Normal file
									
								
							
							
						
						@ -0,0 +1,24 @@
 | 
				
			|||||||
 | 
					.Chain-Tab {
 | 
				
			||||||
 | 
					  display: flex;
 | 
				
			||||||
 | 
					  flex-direction: column;
 | 
				
			||||||
 | 
					  align-items: center;
 | 
				
			||||||
 | 
					  justify-content: center;
 | 
				
			||||||
 | 
					  font-size: 18px;
 | 
				
			||||||
 | 
					  line-height: 20px;
 | 
				
			||||||
 | 
					  height: 32px;
 | 
				
			||||||
 | 
					  width: 32px;
 | 
				
			||||||
 | 
					  color: #fff;
 | 
				
			||||||
 | 
					  cursor: pointer;
 | 
				
			||||||
 | 
					  border-radius: 40px;
 | 
				
			||||||
 | 
					  box-shadow: 0px 1px 3px 0px rgba(0, 0, 0, 0.2);
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					.Chain-Tab:hover {
 | 
				
			||||||
 | 
					  background: #50759e;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					.Chain-Tab-on,
 | 
				
			||||||
 | 
					.Chain-Tab-on:hover {
 | 
				
			||||||
 | 
					  background: #f2e370;
 | 
				
			||||||
 | 
					  color: #1f4671;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
							
								
								
									
										35
									
								
								src/components/Chain/Tab.tsx
									
									
									
									
									
										Normal file
									
								
							
							
						
						@ -0,0 +1,35 @@
 | 
				
			|||||||
 | 
					import * as React from 'react';
 | 
				
			||||||
 | 
					import { ChainDisplay } from './';
 | 
				
			||||||
 | 
					import { Icon } from '../';
 | 
				
			||||||
 | 
					import { setHashData } from '../../utils';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					import './Tab.css';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					interface TabProps {
 | 
				
			||||||
 | 
					  label: string;
 | 
				
			||||||
 | 
					  icon: string;
 | 
				
			||||||
 | 
					  display: ChainDisplay;
 | 
				
			||||||
 | 
					  current: string;
 | 
				
			||||||
 | 
					  tab: string;
 | 
				
			||||||
 | 
					  setDisplay: (display: ChainDisplay) => void;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export class Tab extends React.Component<TabProps> {
 | 
				
			||||||
 | 
					  public render() {
 | 
				
			||||||
 | 
					    const { label, icon, display, current } = this.props;
 | 
				
			||||||
 | 
					    const highlight = display === current;
 | 
				
			||||||
 | 
					    const className = highlight ? 'Chain-Tab-on Chain-Tab' : 'Chain-Tab';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    return (
 | 
				
			||||||
 | 
					      <div className={className} onClick={this.onClick} title={label}>
 | 
				
			||||||
 | 
					        <Icon src={icon} />
 | 
				
			||||||
 | 
					      </div>
 | 
				
			||||||
 | 
					    );
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  private onClick = () => {
 | 
				
			||||||
 | 
					    const { tab, display, setDisplay } = this.props;
 | 
				
			||||||
 | 
					    setHashData({ tab });
 | 
				
			||||||
 | 
					    setDisplay(display);
 | 
				
			||||||
 | 
					  };
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
							
								
								
									
										3
									
								
								src/components/Chain/index.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						@ -0,0 +1,3 @@
 | 
				
			|||||||
 | 
					export * from './Chain';
 | 
				
			||||||
 | 
					export * from './Tab';
 | 
				
			||||||
 | 
					export * from './Header';
 | 
				
			||||||
							
								
								
									
										38
									
								
								src/components/Filter.css
									
									
									
									
									
										Normal file
									
								
							
							
						
						@ -0,0 +1,38 @@
 | 
				
			|||||||
 | 
					.Filter {
 | 
				
			||||||
 | 
					  position: fixed;
 | 
				
			||||||
 | 
					  z-index: 100;
 | 
				
			||||||
 | 
					  bottom: 20px;
 | 
				
			||||||
 | 
					  left: 50%;
 | 
				
			||||||
 | 
					  width: 400px;
 | 
				
			||||||
 | 
					  font-size: 30px;
 | 
				
			||||||
 | 
					  margin-left: -210px;
 | 
				
			||||||
 | 
					  padding: 10px 10px 10px 60px;
 | 
				
			||||||
 | 
					  border-radius: 4px;
 | 
				
			||||||
 | 
					  background: #111;
 | 
				
			||||||
 | 
					  color: #fff;
 | 
				
			||||||
 | 
					  box-shadow: 0 2px 10px rgba(0, 0, 0, 0.5);
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					.Filter-hidden {
 | 
				
			||||||
 | 
					  bottom: -300px;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					.Filter input {
 | 
				
			||||||
 | 
					  padding: 0;
 | 
				
			||||||
 | 
					  margin: 0;
 | 
				
			||||||
 | 
					  border: none;
 | 
				
			||||||
 | 
					  outline: none;
 | 
				
			||||||
 | 
					  width: 350px;
 | 
				
			||||||
 | 
					  font-size: 30px;
 | 
				
			||||||
 | 
					  background: #111;
 | 
				
			||||||
 | 
					  color: #fff;
 | 
				
			||||||
 | 
					  font-family: 'Ubuntu', sans-serif;
 | 
				
			||||||
 | 
					  font-weight: 300;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					.Filter .Icon {
 | 
				
			||||||
 | 
					  position: absolute;
 | 
				
			||||||
 | 
					  left: 13px;
 | 
				
			||||||
 | 
					  top: 17px;
 | 
				
			||||||
 | 
					  font-size: 30px;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
							
								
								
									
										129
									
								
								src/components/Filter.tsx
									
									
									
									
									
										Normal file
									
								
							
							
						
						@ -0,0 +1,129 @@
 | 
				
			|||||||
 | 
					import * as React from 'react';
 | 
				
			||||||
 | 
					import { Maybe } from '../common';
 | 
				
			||||||
 | 
					import { Node } from '../state';
 | 
				
			||||||
 | 
					import { Icon } from './';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					import searchIcon from '../icons/search.svg';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					import './Filter.css';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					interface FilterProps {
 | 
				
			||||||
 | 
					  onChange: (value: Maybe<(node: Node) => boolean>) => void;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					interface FilterState {
 | 
				
			||||||
 | 
					  value: string;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					const ESCAPE_KEY = 27;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export class Filter extends React.Component<FilterProps, FilterState> {
 | 
				
			||||||
 | 
					  public state = {
 | 
				
			||||||
 | 
					    value: '',
 | 
				
			||||||
 | 
					  };
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  private filterInput: HTMLInputElement;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  public componentDidMount() {
 | 
				
			||||||
 | 
					    window.addEventListener('keyup', this.onWindowKeyUp);
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  public componentWillUnmount() {
 | 
				
			||||||
 | 
					    window.removeEventListener('keyup', this.onWindowKeyUp);
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  public shouldComponentUpdate(
 | 
				
			||||||
 | 
					    nextProps: FilterProps,
 | 
				
			||||||
 | 
					    nextState: FilterState
 | 
				
			||||||
 | 
					  ): boolean {
 | 
				
			||||||
 | 
					    return (
 | 
				
			||||||
 | 
					      this.props.onChange !== nextProps.onChange ||
 | 
				
			||||||
 | 
					      this.state.value !== nextState.value
 | 
				
			||||||
 | 
					    );
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  public render() {
 | 
				
			||||||
 | 
					    const { value } = this.state;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    let className = 'Filter';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    if (value === '') {
 | 
				
			||||||
 | 
					      className += ' Filter-hidden';
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    return (
 | 
				
			||||||
 | 
					      <div className={className}>
 | 
				
			||||||
 | 
					        <Icon src={searchIcon} />
 | 
				
			||||||
 | 
					        <input
 | 
				
			||||||
 | 
					          ref={this.onRef}
 | 
				
			||||||
 | 
					          value={value}
 | 
				
			||||||
 | 
					          onChange={this.onChange}
 | 
				
			||||||
 | 
					          onKeyUp={this.onKeyUp}
 | 
				
			||||||
 | 
					        />
 | 
				
			||||||
 | 
					      </div>
 | 
				
			||||||
 | 
					    );
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  private setValue(value: string) {
 | 
				
			||||||
 | 
					    this.setState({ value });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    this.props.onChange(this.getNodeFilter(value));
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  private onRef = (el: HTMLInputElement) => {
 | 
				
			||||||
 | 
					    this.filterInput = el;
 | 
				
			||||||
 | 
					  };
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  private onChange = () => {
 | 
				
			||||||
 | 
					    this.setValue(this.filterInput.value);
 | 
				
			||||||
 | 
					  };
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  private onKeyUp = (event: React.KeyboardEvent<HTMLInputElement>) => {
 | 
				
			||||||
 | 
					    event.stopPropagation();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    if (event.keyCode === ESCAPE_KEY) {
 | 
				
			||||||
 | 
					      this.setValue('');
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					  };
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  private onWindowKeyUp = (event: KeyboardEvent) => {
 | 
				
			||||||
 | 
					    // Ignore if control key is being pressed
 | 
				
			||||||
 | 
					    if (event.ctrlKey) {
 | 
				
			||||||
 | 
					      return;
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					    // Ignore events dispatched to other elements that want to use it
 | 
				
			||||||
 | 
					    // eslint-disable-next-line @typescript-eslint/no-explicit-any
 | 
				
			||||||
 | 
					    if (['INPUT', 'TEXTAREA'].includes((event.target as any)?.tagName)) {
 | 
				
			||||||
 | 
					      return;
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    const { value } = this.state;
 | 
				
			||||||
 | 
					    const key = event.key;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    const escape = value && event.keyCode === ESCAPE_KEY;
 | 
				
			||||||
 | 
					    const singleChar = value === '' && key.length === 1;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    if (escape) {
 | 
				
			||||||
 | 
					      this.setValue('');
 | 
				
			||||||
 | 
					    } else if (singleChar) {
 | 
				
			||||||
 | 
					      this.setValue(key);
 | 
				
			||||||
 | 
					      this.filterInput.focus();
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					  };
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  private getNodeFilter(value: string): Maybe<(node: Node) => boolean> {
 | 
				
			||||||
 | 
					    if (value === '') {
 | 
				
			||||||
 | 
					      return null;
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    const filter = value.toLowerCase();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    return ({ name, city }) => {
 | 
				
			||||||
 | 
					      const matchesName = name.toLowerCase().indexOf(filter) !== -1;
 | 
				
			||||||
 | 
					      const matchesCity =
 | 
				
			||||||
 | 
					        city != null && city.toLowerCase().indexOf(filter) !== -1;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      return matchesName || matchesCity;
 | 
				
			||||||
 | 
					    };
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
							
								
								
									
										15
									
								
								src/components/Icon.css
									
									
									
									
									
										Normal file
									
								
							
							
						
						@ -0,0 +1,15 @@
 | 
				
			|||||||
 | 
					.Icon {
 | 
				
			||||||
 | 
					  fill: currentColor;
 | 
				
			||||||
 | 
					  height: 1em;
 | 
				
			||||||
 | 
					  width: 1em;
 | 
				
			||||||
 | 
					  text-align: center;
 | 
				
			||||||
 | 
					  line-height: 1em;
 | 
				
			||||||
 | 
					  vertical-align: middle;
 | 
				
			||||||
 | 
					  display: inline-block;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					.Icon svg,
 | 
				
			||||||
 | 
					.Icon-symbol-root symbol {
 | 
				
			||||||
 | 
					  width: 1em;
 | 
				
			||||||
 | 
					  height: 1em;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
							
								
								
									
										73
									
								
								src/components/Icon.tsx
									
									
									
									
									
										Normal file
									
								
							
							
						
						@ -0,0 +1,73 @@
 | 
				
			|||||||
 | 
					import * as React from 'react';
 | 
				
			||||||
 | 
					import './Icon.css';
 | 
				
			||||||
 | 
					import { getSVGShadowRoot, W3SVG } from '../utils';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					interface IconProps {
 | 
				
			||||||
 | 
					  src: string;
 | 
				
			||||||
 | 
					  className?: string;
 | 
				
			||||||
 | 
					  onClick?: () => void;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					const symbols = new Map<string, string>();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					let symbolId = 0;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// Lazily render the icon in the DOM, so that we can referenced
 | 
				
			||||||
 | 
					// it by id using shadow DOM.
 | 
				
			||||||
 | 
					function renderShadowIcon(src: string): string {
 | 
				
			||||||
 | 
					  let symbol = symbols.get(src);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  if (!symbol) {
 | 
				
			||||||
 | 
					    symbol = `icon${symbolId}`;
 | 
				
			||||||
 | 
					    symbolId += 1;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    symbols.set(src, symbol);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    fetch(src).then(async (response) => {
 | 
				
			||||||
 | 
					      const html = await response.text();
 | 
				
			||||||
 | 
					      const temp = document.createElement('div');
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      temp.innerHTML = html;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      const tempSVG = temp.querySelector('svg') as SVGSVGElement;
 | 
				
			||||||
 | 
					      const symEl = document.createElementNS(W3SVG, 'symbol');
 | 
				
			||||||
 | 
					      const viewBox = tempSVG.getAttribute('viewBox');
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      symEl.setAttribute('id', symbol as string);
 | 
				
			||||||
 | 
					      if (viewBox) {
 | 
				
			||||||
 | 
					        symEl.setAttribute('viewBox', viewBox);
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      for (const child of Array.from(tempSVG.childNodes)) {
 | 
				
			||||||
 | 
					        symEl.appendChild(child);
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      getSVGShadowRoot().appendChild(symEl);
 | 
				
			||||||
 | 
					    });
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  return symbol;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export class Icon extends React.Component<IconProps> {
 | 
				
			||||||
 | 
					  public props: IconProps;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  public shouldComponentUpdate(nextProps: IconProps) {
 | 
				
			||||||
 | 
					    return (
 | 
				
			||||||
 | 
					      this.props.src !== nextProps.src ||
 | 
				
			||||||
 | 
					      this.props.className !== nextProps.className
 | 
				
			||||||
 | 
					    );
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  public render() {
 | 
				
			||||||
 | 
					    const { className, onClick, src } = this.props;
 | 
				
			||||||
 | 
					    const symbol = renderShadowIcon(src);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    // Use `href` for a shadow DOM reference to the rendered icon
 | 
				
			||||||
 | 
					    return (
 | 
				
			||||||
 | 
					      <svg className={`Icon ${className || ''}`} onClick={onClick}>
 | 
				
			||||||
 | 
					        <use href={`#${symbol}`} />
 | 
				
			||||||
 | 
					      </svg>
 | 
				
			||||||
 | 
					    );
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
							
								
								
									
										46
									
								
								src/components/List/Column/BlockHashColumn.tsx
									
									
									
									
									
										Normal file
									
								
							
							
						
						@ -0,0 +1,46 @@
 | 
				
			|||||||
 | 
					import * as React from 'react';
 | 
				
			||||||
 | 
					import { Maybe } from '../../../common';
 | 
				
			||||||
 | 
					import { ColumnProps } from './';
 | 
				
			||||||
 | 
					import { Node } from '../../../state';
 | 
				
			||||||
 | 
					import { Truncate, Tooltip, TooltipCopyCallback } from '../../';
 | 
				
			||||||
 | 
					import icon from '../../../icons/file-binary.svg';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export class BlockHashColumn extends React.Component<ColumnProps> {
 | 
				
			||||||
 | 
					  public static readonly label = 'Block Hash';
 | 
				
			||||||
 | 
					  public static readonly icon = icon;
 | 
				
			||||||
 | 
					  public static readonly width = 154;
 | 
				
			||||||
 | 
					  public static readonly setting = 'blockhash';
 | 
				
			||||||
 | 
					  public static readonly sortBy = ({ hash }: Node) => hash || '';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  private data: Maybe<string>;
 | 
				
			||||||
 | 
					  private copy: Maybe<TooltipCopyCallback>;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  public shouldComponentUpdate(nextProps: ColumnProps) {
 | 
				
			||||||
 | 
					    return this.data !== nextProps.node.hash;
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  render() {
 | 
				
			||||||
 | 
					    const { hash } = this.props.node;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    this.data = hash;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    return (
 | 
				
			||||||
 | 
					      <td className="Column" onClick={this.onClick}>
 | 
				
			||||||
 | 
					        <Tooltip text={hash} position="right" copy={this.onCopy} />
 | 
				
			||||||
 | 
					        <Truncate text={hash} chars={16} />
 | 
				
			||||||
 | 
					      </td>
 | 
				
			||||||
 | 
					    );
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  private onCopy = (copy: TooltipCopyCallback) => {
 | 
				
			||||||
 | 
					    this.copy = copy;
 | 
				
			||||||
 | 
					  };
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  private onClick = (event: React.MouseEvent) => {
 | 
				
			||||||
 | 
					    event.stopPropagation();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    if (this.copy != null) {
 | 
				
			||||||
 | 
					      this.copy();
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					  };
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
							
								
								
									
										27
									
								
								src/components/List/Column/BlockNumberColumn.tsx
									
									
									
									
									
										Normal file
									
								
							
							
						
						@ -0,0 +1,27 @@
 | 
				
			|||||||
 | 
					import * as React from 'react';
 | 
				
			||||||
 | 
					import { ColumnProps } from './';
 | 
				
			||||||
 | 
					import { Node } from '../../../state';
 | 
				
			||||||
 | 
					import { formatNumber } from '../../../utils';
 | 
				
			||||||
 | 
					import icon from '../../../icons/cube.svg';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export class BlockNumberColumn extends React.Component<ColumnProps> {
 | 
				
			||||||
 | 
					  public static readonly label = 'Block';
 | 
				
			||||||
 | 
					  public static readonly icon = icon;
 | 
				
			||||||
 | 
					  public static readonly width = 88;
 | 
				
			||||||
 | 
					  public static readonly setting = 'blocknumber';
 | 
				
			||||||
 | 
					  public static readonly sortBy = ({ height }: Node) => height;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  private data = 0;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  public shouldComponentUpdate(nextProps: ColumnProps) {
 | 
				
			||||||
 | 
					    return this.data !== nextProps.node.height;
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  render() {
 | 
				
			||||||
 | 
					    const { height } = this.props.node;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    this.data = height;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    return <td className="Column">{`#${formatNumber(height)}`}</td>;
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
							
								
								
									
										31
									
								
								src/components/List/Column/BlockPropagationColumn.tsx
									
									
									
									
									
										Normal file
									
								
							
							
						
						@ -0,0 +1,31 @@
 | 
				
			|||||||
 | 
					import * as React from 'react';
 | 
				
			||||||
 | 
					import { Maybe } from '../../../common';
 | 
				
			||||||
 | 
					import { ColumnProps } from './';
 | 
				
			||||||
 | 
					import { Node } from '../../../state';
 | 
				
			||||||
 | 
					import { milliOrSecond } from '../../../utils';
 | 
				
			||||||
 | 
					import icon from '../../../icons/dashboard.svg';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export class BlockPropagationColumn extends React.Component<ColumnProps> {
 | 
				
			||||||
 | 
					  public static readonly label = 'Block Propagation Time';
 | 
				
			||||||
 | 
					  public static readonly icon = icon;
 | 
				
			||||||
 | 
					  public static readonly width = 58;
 | 
				
			||||||
 | 
					  public static readonly setting = 'blockpropagation';
 | 
				
			||||||
 | 
					  public static readonly sortBy = ({ propagationTime }: Node) =>
 | 
				
			||||||
 | 
					    propagationTime == null ? Infinity : propagationTime;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  private data: Maybe<number>;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  public shouldComponentUpdate(nextProps: ColumnProps) {
 | 
				
			||||||
 | 
					    return this.data !== nextProps.node.propagationTime;
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  render() {
 | 
				
			||||||
 | 
					    const { propagationTime } = this.props.node;
 | 
				
			||||||
 | 
					    const print =
 | 
				
			||||||
 | 
					      propagationTime == null ? '∞' : milliOrSecond(propagationTime);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    this.data = propagationTime;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    return <td className="Column">{print}</td>;
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
							
								
								
									
										30
									
								
								src/components/List/Column/BlockTimeColumn.tsx
									
									
									
									
									
										Normal file
									
								
							
							
						
						@ -0,0 +1,30 @@
 | 
				
			|||||||
 | 
					import * as React from 'react';
 | 
				
			||||||
 | 
					import { ColumnProps } from './';
 | 
				
			||||||
 | 
					import { Node } from '../../../state';
 | 
				
			||||||
 | 
					import { secondsWithPrecision } from '../../../utils';
 | 
				
			||||||
 | 
					import icon from '../../../icons/history.svg';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export class BlockTimeColumn extends React.Component<ColumnProps> {
 | 
				
			||||||
 | 
					  public static readonly label = 'Block Time';
 | 
				
			||||||
 | 
					  public static readonly icon = icon;
 | 
				
			||||||
 | 
					  public static readonly width = 80;
 | 
				
			||||||
 | 
					  public static readonly setting = 'blocktime';
 | 
				
			||||||
 | 
					  public static readonly sortBy = ({ blockTime }: Node) =>
 | 
				
			||||||
 | 
					    blockTime == null ? Infinity : blockTime;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  private data = 0;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  public shouldComponentUpdate(nextProps: ColumnProps) {
 | 
				
			||||||
 | 
					    return this.data !== nextProps.node.blockTime;
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  render() {
 | 
				
			||||||
 | 
					    const { blockTime } = this.props.node;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    this.data = blockTime;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    return (
 | 
				
			||||||
 | 
					      <td className="Column">{`${secondsWithPrecision(blockTime / 1000)}`}</td>
 | 
				
			||||||
 | 
					    );
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
							
								
								
									
										42
									
								
								src/components/List/Column/Column.css
									
									
									
									
									
										Normal file
									
								
							
							
						
						@ -0,0 +1,42 @@
 | 
				
			|||||||
 | 
					.Column {
 | 
				
			||||||
 | 
					  text-align: left;
 | 
				
			||||||
 | 
					  padding: 6px 13px;
 | 
				
			||||||
 | 
					  height: 19px;
 | 
				
			||||||
 | 
					  position: relative;
 | 
				
			||||||
 | 
					  white-space: nowrap;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					.Column-truncate {
 | 
				
			||||||
 | 
					  position: absolute;
 | 
				
			||||||
 | 
					  left: 0;
 | 
				
			||||||
 | 
					  right: 0;
 | 
				
			||||||
 | 
					  top: 0;
 | 
				
			||||||
 | 
					  padding: 6px 13px;
 | 
				
			||||||
 | 
					  overflow: hidden;
 | 
				
			||||||
 | 
					  text-overflow: ellipsis;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					.Column-Tooltip {
 | 
				
			||||||
 | 
					  position: initial !important;
 | 
				
			||||||
 | 
					  padding: inherit !important;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					.Column-validator {
 | 
				
			||||||
 | 
					  display: block;
 | 
				
			||||||
 | 
					  width: 16px;
 | 
				
			||||||
 | 
					  height: 16px;
 | 
				
			||||||
 | 
					  cursor: pointer;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					.Column-validator:hover {
 | 
				
			||||||
 | 
					  transform: scale(2);
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					.Column--a {
 | 
				
			||||||
 | 
					  color: inherit;
 | 
				
			||||||
 | 
					  text-decoration: none;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					.Column--a:hover {
 | 
				
			||||||
 | 
					  text-decoration: underline;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
							
								
								
									
										113
									
								
								src/components/List/Column/Column.tsx
									
									
									
									
									
										Normal file
									
								
							
							
						
						@ -0,0 +1,113 @@
 | 
				
			|||||||
 | 
					import { Types, Maybe, timestamp } from '../../../common';
 | 
				
			||||||
 | 
					import { Node } from '../../../state';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					import './Column.css';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					import {
 | 
				
			||||||
 | 
					  NameColumn,
 | 
				
			||||||
 | 
					  ValidatorColumn,
 | 
				
			||||||
 | 
					  LocationColumn,
 | 
				
			||||||
 | 
					  NetworkIdColumn,
 | 
				
			||||||
 | 
					  PeersColumn,
 | 
				
			||||||
 | 
					  TxsColumn,
 | 
				
			||||||
 | 
					  UploadColumn,
 | 
				
			||||||
 | 
					  DownloadColumn,
 | 
				
			||||||
 | 
					  StateCacheColumn,
 | 
				
			||||||
 | 
					  BlockNumberColumn,
 | 
				
			||||||
 | 
					  BlockHashColumn,
 | 
				
			||||||
 | 
					  FinalizedBlockColumn,
 | 
				
			||||||
 | 
					  FinalizedHashColumn,
 | 
				
			||||||
 | 
					  BlockTimeColumn,
 | 
				
			||||||
 | 
					  BlockPropagationColumn,
 | 
				
			||||||
 | 
					  LastBlockColumn,
 | 
				
			||||||
 | 
					  UptimeColumn,
 | 
				
			||||||
 | 
					  CpuArchitectureColumn, //extra columns added
 | 
				
			||||||
 | 
					  CpuColumn,
 | 
				
			||||||
 | 
					  CpuCoresColumn,
 | 
				
			||||||
 | 
					  LinuxKernelColumn,
 | 
				
			||||||
 | 
					  IsVirtualMachineColumn,
 | 
				
			||||||
 | 
					  MemoryColumn,
 | 
				
			||||||
 | 
					  OperatingSystemColumn,
 | 
				
			||||||
 | 
					  VersionColumn,
 | 
				
			||||||
 | 
					  LinuxDistroColumn,
 | 
				
			||||||
 | 
					} from './';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export type Column =
 | 
				
			||||||
 | 
					  | typeof NameColumn
 | 
				
			||||||
 | 
					  | typeof ValidatorColumn
 | 
				
			||||||
 | 
					  | typeof LocationColumn
 | 
				
			||||||
 | 
					  | typeof NetworkIdColumn
 | 
				
			||||||
 | 
					  | typeof PeersColumn
 | 
				
			||||||
 | 
					  | typeof TxsColumn
 | 
				
			||||||
 | 
					  | typeof UploadColumn
 | 
				
			||||||
 | 
					  | typeof DownloadColumn
 | 
				
			||||||
 | 
					  | typeof StateCacheColumn
 | 
				
			||||||
 | 
					  | typeof BlockNumberColumn
 | 
				
			||||||
 | 
					  | typeof BlockHashColumn
 | 
				
			||||||
 | 
					  | typeof FinalizedBlockColumn
 | 
				
			||||||
 | 
					  | typeof FinalizedHashColumn
 | 
				
			||||||
 | 
					  | typeof BlockTimeColumn
 | 
				
			||||||
 | 
					  | typeof BlockPropagationColumn
 | 
				
			||||||
 | 
					  | typeof LastBlockColumn
 | 
				
			||||||
 | 
					  | typeof UptimeColumn
 | 
				
			||||||
 | 
					  | typeof CpuArchitectureColumn
 | 
				
			||||||
 | 
					  | typeof CpuColumn
 | 
				
			||||||
 | 
					  | typeof CpuCoresColumn
 | 
				
			||||||
 | 
					  | typeof LinuxDistroColumn
 | 
				
			||||||
 | 
					  | typeof LinuxKernelColumn
 | 
				
			||||||
 | 
					  | typeof IsVirtualMachineColumn
 | 
				
			||||||
 | 
					  | typeof MemoryColumn
 | 
				
			||||||
 | 
					  | typeof OperatingSystemColumn
 | 
				
			||||||
 | 
					  | typeof VersionColumn;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export interface ColumnProps {
 | 
				
			||||||
 | 
					  node: Node;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export function formatBytes(
 | 
				
			||||||
 | 
					  bytes: number,
 | 
				
			||||||
 | 
					  stamp: Maybe<Types.Timestamp>
 | 
				
			||||||
 | 
					): string {
 | 
				
			||||||
 | 
					  const ago = stamp ? ` (${formatStamp(stamp)})` : '';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  if (bytes >= 1024 * 1024 * 1024) {
 | 
				
			||||||
 | 
					    return `${(bytes / (1024 * 1024 * 1024)).toFixed(1)} GB${ago}`;
 | 
				
			||||||
 | 
					  } else if (bytes >= 1024 * 1024) {
 | 
				
			||||||
 | 
					    return `${(bytes / (1024 * 1024)).toFixed(1)} MB${ago}`;
 | 
				
			||||||
 | 
					  } else if (bytes >= 1000) {
 | 
				
			||||||
 | 
					    return `${(bytes / 1024).toFixed(1)} kB${ago}`;
 | 
				
			||||||
 | 
					  } else {
 | 
				
			||||||
 | 
					    return `${bytes} B${ago}`;
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export function formatBandwidth(
 | 
				
			||||||
 | 
					  bps: number,
 | 
				
			||||||
 | 
					  stamp: Maybe<Types.Timestamp>
 | 
				
			||||||
 | 
					): string {
 | 
				
			||||||
 | 
					  const ago = stamp ? ` (${formatStamp(stamp)})` : '';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  if (bps >= 1024 * 1024) {
 | 
				
			||||||
 | 
					    return `${(bps / (1024 * 1024)).toFixed(1)} MB/s${ago}`;
 | 
				
			||||||
 | 
					  } else if (bps >= 1000) {
 | 
				
			||||||
 | 
					    return `${(bps / 1024).toFixed(1)} kB/s${ago}`;
 | 
				
			||||||
 | 
					  } else {
 | 
				
			||||||
 | 
					    return `${bps | 0} B/s${ago}`;
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export const BANDWIDTH_SCALE = 1024 * 1024;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					function formatStamp(stamp: Types.Timestamp): string {
 | 
				
			||||||
 | 
					  const passed = ((timestamp() - stamp) / 1000) | 0;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  const hours = (passed / 3600) | 0;
 | 
				
			||||||
 | 
					  const minutes = ((passed % 3600) / 60) | 0;
 | 
				
			||||||
 | 
					  const seconds = passed % 60 | 0;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  return hours
 | 
				
			||||||
 | 
					    ? `${hours}h ago`
 | 
				
			||||||
 | 
					    : minutes
 | 
				
			||||||
 | 
					    ? `${minutes}m ago`
 | 
				
			||||||
 | 
					    : `${seconds}s ago`;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
							
								
								
									
										26
									
								
								src/components/List/Column/CpuArchitectureColumn.tsx
									
									
									
									
									
										Normal file
									
								
							
							
						
						@ -0,0 +1,26 @@
 | 
				
			|||||||
 | 
					import * as React from 'react';
 | 
				
			||||||
 | 
					import { ColumnProps } from './';
 | 
				
			||||||
 | 
					import { Node } from '../../../state';
 | 
				
			||||||
 | 
					import icon from '../../../icons/file-binary.svg';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export class CpuArchitectureColumn extends React.Component<ColumnProps> {
 | 
				
			||||||
 | 
					  public static readonly label = 'CPU Architecture';
 | 
				
			||||||
 | 
					  public static readonly icon = icon;
 | 
				
			||||||
 | 
					  public static readonly width = 154;
 | 
				
			||||||
 | 
					  public static readonly setting = 'target_arch';
 | 
				
			||||||
 | 
					  public static readonly sortBy = ({ target_arch }: Node) => target_arch || '';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  private data: string;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  public shouldComponentUpdate(nextProps: ColumnProps) {
 | 
				
			||||||
 | 
					    return this.data !== nextProps.node.hash;
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  render() {
 | 
				
			||||||
 | 
					    const { target_arch } = this.props.node;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    this.data = target_arch;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    return <td className="Column">{target_arch || '-'}</td>;
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
							
								
								
									
										26
									
								
								src/components/List/Column/CpuColumn.tsx
									
									
									
									
									
										Normal file
									
								
							
							
						
						@ -0,0 +1,26 @@
 | 
				
			|||||||
 | 
					import * as React from 'react';
 | 
				
			||||||
 | 
					import { ColumnProps } from './';
 | 
				
			||||||
 | 
					import { Node } from '../../../state';
 | 
				
			||||||
 | 
					import icon from '../../../icons/file-binary.svg';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export class CpuColumn extends React.Component<ColumnProps> {
 | 
				
			||||||
 | 
					  public static readonly label = 'CPU Column';
 | 
				
			||||||
 | 
					  public static readonly icon = icon;
 | 
				
			||||||
 | 
					  public static readonly width = 154;
 | 
				
			||||||
 | 
					  public static readonly setting = 'cpu';
 | 
				
			||||||
 | 
					  public static readonly sortBy = ({ cpu }: Node) => cpu || 0;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  private data: string;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  public shouldComponentUpdate(nextProps: ColumnProps) {
 | 
				
			||||||
 | 
					    return this.data !== nextProps.node.cpu;
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  render() {
 | 
				
			||||||
 | 
					    const { cpu } = this.props.node;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    this.data = cpu;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    return <td className="Column">{cpu || '-'}</td>;
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
							
								
								
									
										26
									
								
								src/components/List/Column/CpuCoresColumn.tsx
									
									
									
									
									
										Normal file
									
								
							
							
						
						@ -0,0 +1,26 @@
 | 
				
			|||||||
 | 
					import * as React from 'react';
 | 
				
			||||||
 | 
					import { ColumnProps } from './';
 | 
				
			||||||
 | 
					import { Node } from '../../../state';
 | 
				
			||||||
 | 
					import icon from '../../../icons/file-binary.svg';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export class CpuCoresColumn extends React.Component<ColumnProps> {
 | 
				
			||||||
 | 
					  public static readonly label = 'CPU Cores';
 | 
				
			||||||
 | 
					  public static readonly icon = icon;
 | 
				
			||||||
 | 
					  public static readonly width = 154;
 | 
				
			||||||
 | 
					  public static readonly setting = 'core_count';
 | 
				
			||||||
 | 
					  public static readonly sortBy = ({ core_count }: Node) => core_count || 0;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  private data: number;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  public shouldComponentUpdate(nextProps: ColumnProps) {
 | 
				
			||||||
 | 
					    return this.data !== nextProps.node.core_count;
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  render() {
 | 
				
			||||||
 | 
					    const { core_count } = this.props.node;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    this.data = core_count;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    return <td className="Column">{core_count || '-'}</td>;
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
							
								
								
									
										45
									
								
								src/components/List/Column/DownloadColumn.tsx
									
									
									
									
									
										Normal file
									
								
							
							
						
						@ -0,0 +1,45 @@
 | 
				
			|||||||
 | 
					import * as React from 'react';
 | 
				
			||||||
 | 
					import { ColumnProps, formatBandwidth, BANDWIDTH_SCALE } from './';
 | 
				
			||||||
 | 
					import { Node } from '../../../state';
 | 
				
			||||||
 | 
					import { Sparkline } from '../../';
 | 
				
			||||||
 | 
					import icon from '../../../icons/cloud-download.svg';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export class DownloadColumn extends React.Component<ColumnProps> {
 | 
				
			||||||
 | 
					  public static readonly label = 'Download Bandwidth';
 | 
				
			||||||
 | 
					  public static readonly icon = icon;
 | 
				
			||||||
 | 
					  public static readonly width = 40;
 | 
				
			||||||
 | 
					  public static readonly setting = 'download';
 | 
				
			||||||
 | 
					  public static readonly sortBy = ({ download }: Node) =>
 | 
				
			||||||
 | 
					    download.length < 3 ? 0 : download[download.length - 1];
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  private data: Array<number> = [];
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  public shouldComponentUpdate(nextProps: ColumnProps) {
 | 
				
			||||||
 | 
					    // Diffing by ref, as data is an immutable array
 | 
				
			||||||
 | 
					    return this.data !== nextProps.node.download;
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  render() {
 | 
				
			||||||
 | 
					    const { download, chartstamps } = this.props.node;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    this.data = download;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    if (download.length < 3) {
 | 
				
			||||||
 | 
					      return <td className="Column">-</td>;
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    return (
 | 
				
			||||||
 | 
					      <td className="Column">
 | 
				
			||||||
 | 
					        <Sparkline
 | 
				
			||||||
 | 
					          width={44}
 | 
				
			||||||
 | 
					          height={16}
 | 
				
			||||||
 | 
					          stroke={1}
 | 
				
			||||||
 | 
					          format={formatBandwidth}
 | 
				
			||||||
 | 
					          values={download}
 | 
				
			||||||
 | 
					          stamps={chartstamps}
 | 
				
			||||||
 | 
					          minScale={BANDWIDTH_SCALE}
 | 
				
			||||||
 | 
					        />
 | 
				
			||||||
 | 
					      </td>
 | 
				
			||||||
 | 
					    );
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
							
								
								
									
										27
									
								
								src/components/List/Column/FinalizedBlockColumn.tsx
									
									
									
									
									
										Normal file
									
								
							
							
						
						@ -0,0 +1,27 @@
 | 
				
			|||||||
 | 
					import * as React from 'react';
 | 
				
			||||||
 | 
					import { ColumnProps } from './';
 | 
				
			||||||
 | 
					import { Node } from '../../../state';
 | 
				
			||||||
 | 
					import { formatNumber } from '../../../utils';
 | 
				
			||||||
 | 
					import icon from '../../../icons/cube-alt.svg';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export class FinalizedBlockColumn extends React.Component<ColumnProps> {
 | 
				
			||||||
 | 
					  public static readonly label = 'Finalized Block';
 | 
				
			||||||
 | 
					  public static readonly icon = icon;
 | 
				
			||||||
 | 
					  public static readonly width = 88;
 | 
				
			||||||
 | 
					  public static readonly setting = 'finalized';
 | 
				
			||||||
 | 
					  public static readonly sortBy = ({ finalized }: Node) => finalized || 0;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  private data = 0;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  public shouldComponentUpdate(nextProps: ColumnProps) {
 | 
				
			||||||
 | 
					    return this.data !== nextProps.node.finalized;
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  render() {
 | 
				
			||||||
 | 
					    const { finalized } = this.props.node;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    this.data = finalized;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    return <td className="Column">{`#${formatNumber(finalized)}`}</td>;
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
							
								
								
									
										47
									
								
								src/components/List/Column/FinalizedHashColumn.tsx
									
									
									
									
									
										Normal file
									
								
							
							
						
						@ -0,0 +1,47 @@
 | 
				
			|||||||
 | 
					import * as React from 'react';
 | 
				
			||||||
 | 
					import { Maybe } from '../../../common';
 | 
				
			||||||
 | 
					import { ColumnProps } from './';
 | 
				
			||||||
 | 
					import { Node } from '../../../state';
 | 
				
			||||||
 | 
					import { Truncate, Tooltip, TooltipCopyCallback } from '../../';
 | 
				
			||||||
 | 
					import icon from '../../../icons/file-binary.svg';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export class FinalizedHashColumn extends React.Component<ColumnProps> {
 | 
				
			||||||
 | 
					  public static readonly label = 'Finalized Block Hash';
 | 
				
			||||||
 | 
					  public static readonly icon = icon;
 | 
				
			||||||
 | 
					  public static readonly width = 154;
 | 
				
			||||||
 | 
					  public static readonly setting = 'finalizedhash';
 | 
				
			||||||
 | 
					  public static readonly sortBy = ({ finalizedHash }: Node) =>
 | 
				
			||||||
 | 
					    finalizedHash || '';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  private data: Maybe<string>;
 | 
				
			||||||
 | 
					  private copy: Maybe<TooltipCopyCallback>;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  public shouldComponentUpdate(nextProps: ColumnProps) {
 | 
				
			||||||
 | 
					    return this.data !== nextProps.node.finalizedHash;
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  render() {
 | 
				
			||||||
 | 
					    const { finalizedHash } = this.props.node;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    this.data = finalizedHash;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    return (
 | 
				
			||||||
 | 
					      <td className="Column" onClick={this.onClick}>
 | 
				
			||||||
 | 
					        <Tooltip text={finalizedHash} position="right" copy={this.onCopy} />
 | 
				
			||||||
 | 
					        <Truncate text={finalizedHash} chars={16} />
 | 
				
			||||||
 | 
					      </td>
 | 
				
			||||||
 | 
					    );
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  private onCopy = (copy: TooltipCopyCallback) => {
 | 
				
			||||||
 | 
					    this.copy = copy;
 | 
				
			||||||
 | 
					  };
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  private onClick = (event: React.MouseEvent) => {
 | 
				
			||||||
 | 
					    event.stopPropagation();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    if (this.copy != null) {
 | 
				
			||||||
 | 
					      this.copy();
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					  };
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
							
								
								
									
										27
									
								
								src/components/List/Column/IsVirtualMachineColumn.tsx
									
									
									
									
									
										Normal file
									
								
							
							
						
						@ -0,0 +1,27 @@
 | 
				
			|||||||
 | 
					import * as React from 'react';
 | 
				
			||||||
 | 
					import { ColumnProps } from './';
 | 
				
			||||||
 | 
					import { Node } from '../../../state';
 | 
				
			||||||
 | 
					import icon from '../../../icons/file-binary.svg';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export class IsVirtualMachineColumn extends React.Component<ColumnProps> {
 | 
				
			||||||
 | 
					  public static readonly label = 'Virtual Machine';
 | 
				
			||||||
 | 
					  public static readonly icon = icon;
 | 
				
			||||||
 | 
					  public static readonly width = 154;
 | 
				
			||||||
 | 
					  public static readonly setting = 'is_virtual_machine';
 | 
				
			||||||
 | 
					  public static readonly sortBy = ({ is_virtual_machine }: Node) =>
 | 
				
			||||||
 | 
					    is_virtual_machine || false;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  private data: boolean;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  public shouldComponentUpdate(nextProps: ColumnProps) {
 | 
				
			||||||
 | 
					    return this.data !== nextProps.node.is_virtual_machine;
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  render() {
 | 
				
			||||||
 | 
					    const { is_virtual_machine } = this.props.node;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    this.data = is_virtual_machine;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    return <td className="Column">{is_virtual_machine ? 'Yes' : 'No'}</td>;
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
							
								
								
									
										32
									
								
								src/components/List/Column/LastBlockColumn.tsx
									
									
									
									
									
										Normal file
									
								
							
							
						
						@ -0,0 +1,32 @@
 | 
				
			|||||||
 | 
					import * as React from 'react';
 | 
				
			||||||
 | 
					import { ColumnProps } from './';
 | 
				
			||||||
 | 
					import { Node } from '../../../state';
 | 
				
			||||||
 | 
					import { Ago } from '../../';
 | 
				
			||||||
 | 
					import icon from '../../../icons/watch.svg';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export class LastBlockColumn extends React.Component<ColumnProps> {
 | 
				
			||||||
 | 
					  public static readonly label = 'Last Block Time';
 | 
				
			||||||
 | 
					  public static readonly icon = icon;
 | 
				
			||||||
 | 
					  public static readonly width = 100;
 | 
				
			||||||
 | 
					  public static readonly setting = 'blocklasttime';
 | 
				
			||||||
 | 
					  public static readonly sortBy = ({ blockTimestamp }: Node) =>
 | 
				
			||||||
 | 
					    blockTimestamp || 0;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  private data = 0;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  public shouldComponentUpdate(nextProps: ColumnProps) {
 | 
				
			||||||
 | 
					    return this.data !== nextProps.node.blockTimestamp;
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  render() {
 | 
				
			||||||
 | 
					    const { blockTimestamp } = this.props.node;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    this.data = blockTimestamp;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    return (
 | 
				
			||||||
 | 
					      <td className="Column">
 | 
				
			||||||
 | 
					        <Ago when={blockTimestamp} />
 | 
				
			||||||
 | 
					      </td>
 | 
				
			||||||
 | 
					    );
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
							
								
								
									
										27
									
								
								src/components/List/Column/LinuxDistroColumn.tsx
									
									
									
									
									
										Normal file
									
								
							
							
						
						@ -0,0 +1,27 @@
 | 
				
			|||||||
 | 
					import * as React from 'react';
 | 
				
			||||||
 | 
					import { ColumnProps } from './';
 | 
				
			||||||
 | 
					import { Node } from '../../../state';
 | 
				
			||||||
 | 
					import icon from '../../../icons/file-binary.svg';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export class LinuxDistroColumn extends React.Component<ColumnProps> {
 | 
				
			||||||
 | 
					  public static readonly label = 'Linux Distro';
 | 
				
			||||||
 | 
					  public static readonly icon = icon;
 | 
				
			||||||
 | 
					  public static readonly width = 154;
 | 
				
			||||||
 | 
					  public static readonly setting = 'linux_distro';
 | 
				
			||||||
 | 
					  public static readonly sortBy = ({ linux_distro }: Node) =>
 | 
				
			||||||
 | 
					    linux_distro || '';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  private data: string;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  public shouldComponentUpdate(nextProps: ColumnProps) {
 | 
				
			||||||
 | 
					    return this.data !== nextProps.node.linux_distro;
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  render() {
 | 
				
			||||||
 | 
					    const { linux_distro } = this.props.node;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    this.data = linux_distro;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    return <td className="Column">{linux_distro || '-'}</td>;
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
							
								
								
									
										26
									
								
								src/components/List/Column/LinuxKernelColumn.tsx
									
									
									
									
									
										Normal file
									
								
							
							
						
						@ -0,0 +1,26 @@
 | 
				
			|||||||
 | 
					import * as React from 'react';
 | 
				
			||||||
 | 
					import { ColumnProps } from './';
 | 
				
			||||||
 | 
					import { Node } from '../../../state';
 | 
				
			||||||
 | 
					import icon from '../../../icons/file-binary.svg';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export class LinuxKernelColumn extends React.Component<ColumnProps> {
 | 
				
			||||||
 | 
					  public static readonly label = 'Linux Kernel';
 | 
				
			||||||
 | 
					  public static readonly icon = icon;
 | 
				
			||||||
 | 
					  public static readonly width = 154;
 | 
				
			||||||
 | 
					  public static readonly setting = 'linux_kernel';
 | 
				
			||||||
 | 
					  public static readonly sortBy = ({ linux_kernel }: Node) => linux_kernel || 0;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  private data: string;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  public shouldComponentUpdate(nextProps: ColumnProps) {
 | 
				
			||||||
 | 
					    return this.data !== nextProps.node.linux_kernel;
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  render() {
 | 
				
			||||||
 | 
					    const { linux_kernel } = this.props.node;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    this.data = linux_kernel;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    return <td className="Column">{linux_kernel || '-'}</td>;
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
							
								
								
									
										37
									
								
								src/components/List/Column/LocationColumn.tsx
									
									
									
									
									
										Normal file
									
								
							
							
						
						@ -0,0 +1,37 @@
 | 
				
			|||||||
 | 
					import * as React from 'react';
 | 
				
			||||||
 | 
					import { Maybe } from '../../../common';
 | 
				
			||||||
 | 
					import { ColumnProps } from './';
 | 
				
			||||||
 | 
					import { Node } from '../../../state';
 | 
				
			||||||
 | 
					import { Truncate, Tooltip } from '../../';
 | 
				
			||||||
 | 
					import icon from '../../../icons/location.svg';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export class LocationColumn extends React.Component<ColumnProps> {
 | 
				
			||||||
 | 
					  public static readonly label = 'Location';
 | 
				
			||||||
 | 
					  public static readonly icon = icon;
 | 
				
			||||||
 | 
					  public static readonly width = 140;
 | 
				
			||||||
 | 
					  public static readonly setting = 'location';
 | 
				
			||||||
 | 
					  public static readonly sortBy = ({ city }: Node) => city || '';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  private data: Maybe<string>;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  public shouldComponentUpdate(nextProps: ColumnProps) {
 | 
				
			||||||
 | 
					    return this.data !== nextProps.node.city;
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  render() {
 | 
				
			||||||
 | 
					    const { city } = this.props.node;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    this.data = city;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    if (!city) {
 | 
				
			||||||
 | 
					      return <td className="Column">-</td>;
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    return (
 | 
				
			||||||
 | 
					      <td className="Column">
 | 
				
			||||||
 | 
					        <Tooltip text={city} position="left" />
 | 
				
			||||||
 | 
					        <Truncate text={city} chars={14} />
 | 
				
			||||||
 | 
					      </td>
 | 
				
			||||||
 | 
					    );
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
							
								
								
									
										26
									
								
								src/components/List/Column/MemoryColumn.tsx
									
									
									
									
									
										Normal file
									
								
							
							
						
						@ -0,0 +1,26 @@
 | 
				
			|||||||
 | 
					import * as React from 'react';
 | 
				
			||||||
 | 
					import { ColumnProps } from './';
 | 
				
			||||||
 | 
					import { Node } from '../../../state';
 | 
				
			||||||
 | 
					import icon from '../../../icons/file-binary.svg';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export class MemoryColumn extends React.Component<ColumnProps> {
 | 
				
			||||||
 | 
					  public static readonly label = 'memory';
 | 
				
			||||||
 | 
					  public static readonly icon = icon;
 | 
				
			||||||
 | 
					  public static readonly width = 154;
 | 
				
			||||||
 | 
					  public static readonly setting = 'memory';
 | 
				
			||||||
 | 
					  public static readonly sortBy = ({ memory }: Node) => memory || '';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  private data: number;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  public shouldComponentUpdate(nextProps: ColumnProps) {
 | 
				
			||||||
 | 
					    return this.data !== nextProps.node.memory;
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  render() {
 | 
				
			||||||
 | 
					    const { memory } = this.props.node;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    this.data = memory;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    return <td className="Column">{memory || '-'}</td>;
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
							
								
								
									
										29
									
								
								src/components/List/Column/NameColumn.tsx
									
									
									
									
									
										Normal file
									
								
							
							
						
						@ -0,0 +1,29 @@
 | 
				
			|||||||
 | 
					import * as React from 'react';
 | 
				
			||||||
 | 
					import { ColumnProps } from './';
 | 
				
			||||||
 | 
					import { Node } from '../../../state';
 | 
				
			||||||
 | 
					import { Truncate, Tooltip } from '../../';
 | 
				
			||||||
 | 
					import icon from '../../../icons/server.svg';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export class NameColumn extends React.Component<ColumnProps> {
 | 
				
			||||||
 | 
					  public static readonly label = 'Node';
 | 
				
			||||||
 | 
					  public static readonly icon = icon;
 | 
				
			||||||
 | 
					  public static readonly setting = null;
 | 
				
			||||||
 | 
					  public static readonly width = null;
 | 
				
			||||||
 | 
					  public static readonly sortBy = ({ sortableName }: Node) => sortableName;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  public shouldComponentUpdate(nextProps: ColumnProps) {
 | 
				
			||||||
 | 
					    // Node name only changes when the node does
 | 
				
			||||||
 | 
					    return this.props.node !== nextProps.node;
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  render() {
 | 
				
			||||||
 | 
					    const { name } = this.props.node;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    return (
 | 
				
			||||||
 | 
					      <td className="Column">
 | 
				
			||||||
 | 
					        <Tooltip text={name} position="left" />
 | 
				
			||||||
 | 
					        <Truncate text={name} />
 | 
				
			||||||
 | 
					      </td>
 | 
				
			||||||
 | 
					    );
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
							
								
								
									
										50
									
								
								src/components/List/Column/NetworkIdColumn.tsx
									
									
									
									
									
										Normal file
									
								
							
							
						
						@ -0,0 +1,50 @@
 | 
				
			|||||||
 | 
					import * as React from 'react';
 | 
				
			||||||
 | 
					import { Maybe } from '../../../common';
 | 
				
			||||||
 | 
					import { ColumnProps } from './';
 | 
				
			||||||
 | 
					import { Node } from '../../../state';
 | 
				
			||||||
 | 
					import { Tooltip, TooltipCopyCallback } from '../../';
 | 
				
			||||||
 | 
					import icon from '../../../icons/fingerprint.svg';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export class NetworkIdColumn extends React.Component<ColumnProps> {
 | 
				
			||||||
 | 
					  public static readonly label = 'Network ID';
 | 
				
			||||||
 | 
					  public static readonly icon = icon;
 | 
				
			||||||
 | 
					  public static readonly width = 90;
 | 
				
			||||||
 | 
					  public static readonly setting = 'networkId';
 | 
				
			||||||
 | 
					  public static readonly sortBy = ({ networkId }: Node) => networkId || '';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  private data: Maybe<string>;
 | 
				
			||||||
 | 
					  private copy: Maybe<TooltipCopyCallback>;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  public shouldComponentUpdate(nextProps: ColumnProps) {
 | 
				
			||||||
 | 
					    return this.data !== nextProps.node.networkId;
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  render() {
 | 
				
			||||||
 | 
					    const { networkId } = this.props.node;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    this.data = networkId;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    if (!networkId) {
 | 
				
			||||||
 | 
					      return <td className="Column">-</td>;
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    return (
 | 
				
			||||||
 | 
					      <td className="Column" onClick={this.onClick}>
 | 
				
			||||||
 | 
					        <Tooltip text={networkId} position="left" copy={this.onCopy} />
 | 
				
			||||||
 | 
					        {networkId}
 | 
				
			||||||
 | 
					      </td>
 | 
				
			||||||
 | 
					    );
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  private onCopy = (copy: TooltipCopyCallback) => {
 | 
				
			||||||
 | 
					    this.copy = copy;
 | 
				
			||||||
 | 
					  };
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  private onClick = (event: React.MouseEvent) => {
 | 
				
			||||||
 | 
					    event.stopPropagation();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    if (this.copy != null) {
 | 
				
			||||||
 | 
					      this.copy();
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					  };
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
							
								
								
									
										26
									
								
								src/components/List/Column/OperatingSystemColumn.tsx
									
									
									
									
									
										Normal file
									
								
							
							
						
						@ -0,0 +1,26 @@
 | 
				
			|||||||
 | 
					import * as React from 'react';
 | 
				
			||||||
 | 
					import { ColumnProps } from './';
 | 
				
			||||||
 | 
					import { Node } from '../../../state';
 | 
				
			||||||
 | 
					import icon from '../../../icons/file-binary.svg';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export class OperatingSystemColumn extends React.Component<ColumnProps> {
 | 
				
			||||||
 | 
					  public static readonly label = 'OS';
 | 
				
			||||||
 | 
					  public static readonly icon = icon;
 | 
				
			||||||
 | 
					  public static readonly width = 154;
 | 
				
			||||||
 | 
					  public static readonly setting = 'target_os';
 | 
				
			||||||
 | 
					  public static readonly sortBy = ({ target_os }: Node) => target_os || '';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  private data: string;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  public shouldComponentUpdate(nextProps: ColumnProps) {
 | 
				
			||||||
 | 
					    return this.data !== nextProps.node.hash;
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  render() {
 | 
				
			||||||
 | 
					    const { target_os } = this.props.node;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    this.data = target_os;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    return <td className="Column">{target_os || '-'}</td>;
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
							
								
								
									
										26
									
								
								src/components/List/Column/PeersColumn.tsx
									
									
									
									
									
										Normal file
									
								
							
							
						
						@ -0,0 +1,26 @@
 | 
				
			|||||||
 | 
					import * as React from 'react';
 | 
				
			||||||
 | 
					import { ColumnProps } from './';
 | 
				
			||||||
 | 
					import { Node } from '../../../state';
 | 
				
			||||||
 | 
					import icon from '../../../icons/broadcast.svg';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export class PeersColumn extends React.Component<ColumnProps> {
 | 
				
			||||||
 | 
					  public static readonly label = 'Peer Count';
 | 
				
			||||||
 | 
					  public static readonly icon = icon;
 | 
				
			||||||
 | 
					  public static readonly width = 26;
 | 
				
			||||||
 | 
					  public static readonly setting = 'peers';
 | 
				
			||||||
 | 
					  public static readonly sortBy = ({ peers }: Node) => peers;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  private data = 0;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  public shouldComponentUpdate(nextProps: ColumnProps) {
 | 
				
			||||||
 | 
					    return this.data !== nextProps.node.peers;
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  render() {
 | 
				
			||||||
 | 
					    const { peers } = this.props.node;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    this.data = peers;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    return <td className="Column">{peers}</td>;
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
							
								
								
									
										45
									
								
								src/components/List/Column/StateCacheColumn.tsx
									
									
									
									
									
										Normal file
									
								
							
							
						
						@ -0,0 +1,45 @@
 | 
				
			|||||||
 | 
					import * as React from 'react';
 | 
				
			||||||
 | 
					import { ColumnProps, formatBytes, BANDWIDTH_SCALE } from './';
 | 
				
			||||||
 | 
					import { Node } from '../../../state';
 | 
				
			||||||
 | 
					import { Sparkline } from '../../';
 | 
				
			||||||
 | 
					import icon from '../../../icons/git-branch.svg';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export class StateCacheColumn extends React.Component<ColumnProps> {
 | 
				
			||||||
 | 
					  public static readonly label = 'State Cache Size';
 | 
				
			||||||
 | 
					  public static readonly icon = icon;
 | 
				
			||||||
 | 
					  public static readonly width = 40;
 | 
				
			||||||
 | 
					  public static readonly setting = 'stateCacheSize';
 | 
				
			||||||
 | 
					  public static readonly sortBy = ({ stateCacheSize }: Node) =>
 | 
				
			||||||
 | 
					    stateCacheSize.length < 3 ? 0 : stateCacheSize[stateCacheSize.length - 1];
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  private data: Array<number> = [];
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  public shouldComponentUpdate(nextProps: ColumnProps) {
 | 
				
			||||||
 | 
					    // Diffing by ref, as data is an immutable array
 | 
				
			||||||
 | 
					    return this.data !== nextProps.node.stateCacheSize;
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  render() {
 | 
				
			||||||
 | 
					    const { stateCacheSize, chartstamps } = this.props.node;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    this.data = stateCacheSize;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    if (stateCacheSize.length < 3) {
 | 
				
			||||||
 | 
					      return <td className="Column">-</td>;
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    return (
 | 
				
			||||||
 | 
					      <td className="Column">
 | 
				
			||||||
 | 
					        <Sparkline
 | 
				
			||||||
 | 
					          width={44}
 | 
				
			||||||
 | 
					          height={16}
 | 
				
			||||||
 | 
					          stroke={1}
 | 
				
			||||||
 | 
					          format={formatBytes}
 | 
				
			||||||
 | 
					          values={stateCacheSize}
 | 
				
			||||||
 | 
					          stamps={chartstamps}
 | 
				
			||||||
 | 
					          minScale={BANDWIDTH_SCALE}
 | 
				
			||||||
 | 
					        />
 | 
				
			||||||
 | 
					      </td>
 | 
				
			||||||
 | 
					    );
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
							
								
								
									
										26
									
								
								src/components/List/Column/TxsColumn.tsx
									
									
									
									
									
										Normal file
									
								
							
							
						
						@ -0,0 +1,26 @@
 | 
				
			|||||||
 | 
					import * as React from 'react';
 | 
				
			||||||
 | 
					import { ColumnProps } from './';
 | 
				
			||||||
 | 
					import { Node } from '../../../state';
 | 
				
			||||||
 | 
					import icon from '../../../icons/inbox.svg';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export class TxsColumn extends React.Component<ColumnProps> {
 | 
				
			||||||
 | 
					  public static readonly label = 'Transactions in Queue';
 | 
				
			||||||
 | 
					  public static readonly icon = icon;
 | 
				
			||||||
 | 
					  public static readonly width = 26;
 | 
				
			||||||
 | 
					  public static readonly setting = 'txs';
 | 
				
			||||||
 | 
					  public static readonly sortBy = ({ txs }: Node) => txs;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  private data = 0;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  public shouldComponentUpdate(nextProps: ColumnProps) {
 | 
				
			||||||
 | 
					    return this.data !== nextProps.node.txs;
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  render() {
 | 
				
			||||||
 | 
					    const { txs } = this.props.node;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    this.data = txs;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    return <td className="Column">{txs}</td>;
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
							
								
								
									
										45
									
								
								src/components/List/Column/UploadColumn.tsx
									
									
									
									
									
										Normal file
									
								
							
							
						
						@ -0,0 +1,45 @@
 | 
				
			|||||||
 | 
					import * as React from 'react';
 | 
				
			||||||
 | 
					import { ColumnProps, formatBandwidth, BANDWIDTH_SCALE } from './';
 | 
				
			||||||
 | 
					import { Node } from '../../../state';
 | 
				
			||||||
 | 
					import { Sparkline } from '../../';
 | 
				
			||||||
 | 
					import icon from '../../../icons/cloud-upload.svg';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export class UploadColumn extends React.Component<ColumnProps> {
 | 
				
			||||||
 | 
					  public static readonly label = 'Upload Bandwidth';
 | 
				
			||||||
 | 
					  public static readonly icon = icon;
 | 
				
			||||||
 | 
					  public static readonly width = 40;
 | 
				
			||||||
 | 
					  public static readonly setting = 'upload';
 | 
				
			||||||
 | 
					  public static readonly sortBy = ({ upload }: Node) =>
 | 
				
			||||||
 | 
					    upload.length < 3 ? 0 : upload[upload.length - 1];
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  private data: Array<number> = [];
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  public shouldComponentUpdate(nextProps: ColumnProps) {
 | 
				
			||||||
 | 
					    // Diffing by ref, as data is an immutable array
 | 
				
			||||||
 | 
					    return this.data !== nextProps.node.upload;
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  render() {
 | 
				
			||||||
 | 
					    const { upload, chartstamps } = this.props.node;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    this.data = upload;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    if (upload.length < 3) {
 | 
				
			||||||
 | 
					      return <td className="Column">-</td>;
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    return (
 | 
				
			||||||
 | 
					      <td className="Column">
 | 
				
			||||||
 | 
					        <Sparkline
 | 
				
			||||||
 | 
					          width={44}
 | 
				
			||||||
 | 
					          height={16}
 | 
				
			||||||
 | 
					          stroke={1}
 | 
				
			||||||
 | 
					          format={formatBandwidth}
 | 
				
			||||||
 | 
					          values={upload}
 | 
				
			||||||
 | 
					          stamps={chartstamps}
 | 
				
			||||||
 | 
					          minScale={BANDWIDTH_SCALE}
 | 
				
			||||||
 | 
					        />
 | 
				
			||||||
 | 
					      </td>
 | 
				
			||||||
 | 
					    );
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
							
								
								
									
										32
									
								
								src/components/List/Column/UptimeColumn.tsx
									
									
									
									
									
										Normal file
									
								
							
							
						
						@ -0,0 +1,32 @@
 | 
				
			|||||||
 | 
					import * as React from 'react';
 | 
				
			||||||
 | 
					import { ColumnProps } from './';
 | 
				
			||||||
 | 
					import { Node } from '../../../state';
 | 
				
			||||||
 | 
					import { Ago } from '../../';
 | 
				
			||||||
 | 
					import icon from '../../../icons/pulse.svg';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export class UptimeColumn extends React.Component<ColumnProps> {
 | 
				
			||||||
 | 
					  public static readonly label = 'Node Uptime';
 | 
				
			||||||
 | 
					  public static readonly icon = icon;
 | 
				
			||||||
 | 
					  public static readonly width = 58;
 | 
				
			||||||
 | 
					  public static readonly setting = 'uptime';
 | 
				
			||||||
 | 
					  public static readonly sortBy = ({ startupTime }: Node) => startupTime || 0;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  public shouldComponentUpdate(nextProps: ColumnProps) {
 | 
				
			||||||
 | 
					    // Uptime only changes when the node does
 | 
				
			||||||
 | 
					    return this.props.node !== nextProps.node;
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  render() {
 | 
				
			||||||
 | 
					    const { startupTime } = this.props.node;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    if (!startupTime) {
 | 
				
			||||||
 | 
					      return <td className="Column">-</td>;
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    return (
 | 
				
			||||||
 | 
					      <td className="Column">
 | 
				
			||||||
 | 
					        <Ago when={startupTime} justTime={true} />
 | 
				
			||||||
 | 
					      </td>
 | 
				
			||||||
 | 
					    );
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
							
								
								
									
										49
									
								
								src/components/List/Column/ValidatorColumn.tsx
									
									
									
									
									
										Normal file
									
								
							
							
						
						@ -0,0 +1,49 @@
 | 
				
			|||||||
 | 
					import * as React from 'react';
 | 
				
			||||||
 | 
					import { Maybe } from '../../../common';
 | 
				
			||||||
 | 
					import { ColumnProps } from './';
 | 
				
			||||||
 | 
					import { Node } from '../../../state';
 | 
				
			||||||
 | 
					import { Tooltip, TooltipCopyCallback } from '../../';
 | 
				
			||||||
 | 
					import icon from '../../../icons/shield.svg';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export class ValidatorColumn extends React.Component<ColumnProps> {
 | 
				
			||||||
 | 
					  public static readonly label = 'Validator';
 | 
				
			||||||
 | 
					  public static readonly icon = icon;
 | 
				
			||||||
 | 
					  public static readonly width = 16;
 | 
				
			||||||
 | 
					  public static readonly setting = 'validator';
 | 
				
			||||||
 | 
					  public static readonly sortBy = ({ validator }: Node) => validator || '';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  private data: Maybe<string>;
 | 
				
			||||||
 | 
					  private copy: Maybe<TooltipCopyCallback>;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  public shouldComponentUpdate(nextProps: ColumnProps) {
 | 
				
			||||||
 | 
					    return this.data !== nextProps.node.validator;
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  render() {
 | 
				
			||||||
 | 
					    const { validator } = this.props.node;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    this.data = validator;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    if (!validator) {
 | 
				
			||||||
 | 
					      return <td className="Column">-</td>;
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    return (
 | 
				
			||||||
 | 
					      <td className="Column" onClick={this.onClick}>
 | 
				
			||||||
 | 
					        <Tooltip text={validator} copy={this.onCopy} />
 | 
				
			||||||
 | 
					      </td>
 | 
				
			||||||
 | 
					    );
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  private onCopy = (copy: TooltipCopyCallback) => {
 | 
				
			||||||
 | 
					    this.copy = copy;
 | 
				
			||||||
 | 
					  };
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  private onClick = (event: React.MouseEvent) => {
 | 
				
			||||||
 | 
					    event.stopPropagation();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    if (this.copy != null) {
 | 
				
			||||||
 | 
					      this.copy();
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					  };
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
							
								
								
									
										26
									
								
								src/components/List/Column/VersionColumn.tsx
									
									
									
									
									
										Normal file
									
								
							
							
						
						@ -0,0 +1,26 @@
 | 
				
			|||||||
 | 
					import * as React from 'react';
 | 
				
			||||||
 | 
					import { ColumnProps } from './';
 | 
				
			||||||
 | 
					import { Node } from '../../../state';
 | 
				
			||||||
 | 
					import icon from '../../../icons/file-binary.svg';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export class VersionColumn extends React.Component<ColumnProps> {
 | 
				
			||||||
 | 
					  public static readonly label = 'version';
 | 
				
			||||||
 | 
					  public static readonly icon = icon;
 | 
				
			||||||
 | 
					  public static readonly width = 154;
 | 
				
			||||||
 | 
					  public static readonly setting = 'version';
 | 
				
			||||||
 | 
					  public static readonly sortBy = ({ version }: Node) => version || '';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  private data: string;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  public shouldComponentUpdate(nextProps: ColumnProps) {
 | 
				
			||||||
 | 
					    return this.data !== nextProps.node.version;
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  render() {
 | 
				
			||||||
 | 
					    const { version } = this.props.node;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    this.data = version;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    return <td className="Column">{version || '-'}</td>;
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
							
								
								
									
										27
									
								
								src/components/List/Column/index.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						@ -0,0 +1,27 @@
 | 
				
			|||||||
 | 
					export * from './Column';
 | 
				
			||||||
 | 
					export * from './NameColumn';
 | 
				
			||||||
 | 
					export * from './ValidatorColumn';
 | 
				
			||||||
 | 
					export * from './LocationColumn';
 | 
				
			||||||
 | 
					export * from './NetworkIdColumn';
 | 
				
			||||||
 | 
					export * from './PeersColumn';
 | 
				
			||||||
 | 
					export * from './TxsColumn';
 | 
				
			||||||
 | 
					export * from './UploadColumn';
 | 
				
			||||||
 | 
					export * from './DownloadColumn';
 | 
				
			||||||
 | 
					export * from './StateCacheColumn';
 | 
				
			||||||
 | 
					export * from './BlockNumberColumn';
 | 
				
			||||||
 | 
					export * from './BlockHashColumn';
 | 
				
			||||||
 | 
					export * from './FinalizedBlockColumn';
 | 
				
			||||||
 | 
					export * from './FinalizedHashColumn';
 | 
				
			||||||
 | 
					export * from './BlockTimeColumn';
 | 
				
			||||||
 | 
					export * from './BlockPropagationColumn';
 | 
				
			||||||
 | 
					export * from './LastBlockColumn';
 | 
				
			||||||
 | 
					export * from './UptimeColumn';
 | 
				
			||||||
 | 
					export * from './CpuArchitectureColumn';
 | 
				
			||||||
 | 
					export * from './CpuCoresColumn';
 | 
				
			||||||
 | 
					export * from './LinuxDistroColumn';
 | 
				
			||||||
 | 
					export * from './IsVirtualMachineColumn';
 | 
				
			||||||
 | 
					export * from './MemoryColumn';
 | 
				
			||||||
 | 
					export * from './CpuColumn';
 | 
				
			||||||
 | 
					export * from './OperatingSystemColumn';
 | 
				
			||||||
 | 
					export * from './VersionColumn';
 | 
				
			||||||
 | 
					export * from './LinuxKernelColumn';
 | 
				
			||||||
							
								
								
									
										30
									
								
								src/components/List/List.css
									
									
									
									
									
										Normal file
									
								
							
							
						
						@ -0,0 +1,30 @@
 | 
				
			|||||||
 | 
					.List-container {
 | 
				
			||||||
 | 
					  background-color: #345987;
 | 
				
			||||||
 | 
					  height: 100%;
 | 
				
			||||||
 | 
					  min-height: calc(100vh - 148px);
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					.List {
 | 
				
			||||||
 | 
					  /* Prevents the list from auto-scrolling while cascading node
 | 
				
			||||||
 | 
					   * updates on new block, which helps with performance. */
 | 
				
			||||||
 | 
					  overflow-anchor: none;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					.List-no-nodes {
 | 
				
			||||||
 | 
					  font-size: 30px;
 | 
				
			||||||
 | 
					  padding-top: 20vh;
 | 
				
			||||||
 | 
					  text-align: center;
 | 
				
			||||||
 | 
					  font-weight: 300;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					.List-padding {
 | 
				
			||||||
 | 
					  padding: 0;
 | 
				
			||||||
 | 
					  margin: 0;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					.List--table {
 | 
				
			||||||
 | 
					  width: 100%;
 | 
				
			||||||
 | 
					  background-color: #345987;
 | 
				
			||||||
 | 
					  border-spacing: 0;
 | 
				
			||||||
 | 
					  font-family: 'Ubuntu', sans-serif;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
							
								
								
									
										205
									
								
								src/components/List/List.tsx
									
									
									
									
									
										Normal file
									
								
							
							
						
						@ -0,0 +1,205 @@
 | 
				
			|||||||
 | 
					import * as React from 'react';
 | 
				
			||||||
 | 
					import ReactGA from "react-ga4";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					import { Types, Maybe } from '../../common';
 | 
				
			||||||
 | 
					import { Filter } from '../';
 | 
				
			||||||
 | 
					import { State as AppState, Update as AppUpdate, Node } from '../../state';
 | 
				
			||||||
 | 
					import { Row, THead } from './';
 | 
				
			||||||
 | 
					import { Persistent, PersistentSet } from '../../persist';
 | 
				
			||||||
 | 
					import { viewport } from '../../utils';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					const HEADER = 148;
 | 
				
			||||||
 | 
					const TH_HEIGHT = 35;
 | 
				
			||||||
 | 
					const TR_HEIGHT = 31;
 | 
				
			||||||
 | 
					const ROW_MARGIN = 5;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					import './List.css';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					interface ListProps {
 | 
				
			||||||
 | 
					  appState: Readonly<AppState>;
 | 
				
			||||||
 | 
					  appUpdate: AppUpdate;
 | 
				
			||||||
 | 
					  pins: PersistentSet<Types.NodeName>;
 | 
				
			||||||
 | 
					  sortBy: Persistent<Maybe<number>>;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// Helper for readability, used as `key` prop for each `Row`
 | 
				
			||||||
 | 
					// of the `List`, so that we can maximize re-using DOM elements.
 | 
				
			||||||
 | 
					type Key = number;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export class List extends React.Component<ListProps> {
 | 
				
			||||||
 | 
					  public state = {
 | 
				
			||||||
 | 
					    filter: null,
 | 
				
			||||||
 | 
					    viewportHeight: viewport().height,
 | 
				
			||||||
 | 
					  };
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  private listStart = 0;
 | 
				
			||||||
 | 
					  private listEnd = 0;
 | 
				
			||||||
 | 
					  private relativeTop = -1;
 | 
				
			||||||
 | 
					  private nextKey: Key = 0;
 | 
				
			||||||
 | 
					  private previousKeys = new Map<Types.NodeId, Key>();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  public componentDidMount() {
 | 
				
			||||||
 | 
					    ReactGA.send({ hitType: "pageview", page: "/list" });
 | 
				
			||||||
 | 
					    this.onScroll();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    window.addEventListener('resize', this.onResize);
 | 
				
			||||||
 | 
					    window.addEventListener('scroll', this.onScroll);
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  public componentWillUnmount() {
 | 
				
			||||||
 | 
					    window.removeEventListener('resize', this.onResize);
 | 
				
			||||||
 | 
					    window.removeEventListener('scroll', this.onScroll);
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  public render() {
 | 
				
			||||||
 | 
					    const { pins, sortBy, appState } = this.props;
 | 
				
			||||||
 | 
					    const { selectedColumns } = appState;
 | 
				
			||||||
 | 
					    const { filter } = this.state;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    let nodes = appState.nodes.sorted();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    if (filter != null) {
 | 
				
			||||||
 | 
					      nodes = nodes.filter(filter);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      if (nodes.length === 0) {
 | 
				
			||||||
 | 
					        return (
 | 
				
			||||||
 | 
					          <React.Fragment>
 | 
				
			||||||
 | 
					            <div className="List List-no-nodes">
 | 
				
			||||||
 | 
					              ¯\_(ツ)_/¯
 | 
				
			||||||
 | 
					              <br />
 | 
				
			||||||
 | 
					              Nothing matches
 | 
				
			||||||
 | 
					            </div>
 | 
				
			||||||
 | 
					            <Filter onChange={this.onFilterChange} />
 | 
				
			||||||
 | 
					          </React.Fragment>
 | 
				
			||||||
 | 
					        );
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					      // With filter present, we can no longer guarantee that focus corresponds
 | 
				
			||||||
 | 
					      // to rendering view, so we put the whole list in focus
 | 
				
			||||||
 | 
					      appState.nodes.setFocus(0, nodes.length);
 | 
				
			||||||
 | 
					    } else {
 | 
				
			||||||
 | 
					      appState.nodes.setFocus(this.listStart, this.listEnd);
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    const height = TH_HEIGHT + nodes.length * TR_HEIGHT;
 | 
				
			||||||
 | 
					    const top = this.listStart * TR_HEIGHT;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    nodes = nodes.slice(this.listStart, this.listEnd);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    const keys = this.recalculateKeys(nodes);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    return (
 | 
				
			||||||
 | 
					      <div className="List-container">
 | 
				
			||||||
 | 
					        <div className="List" style={{ height }}>
 | 
				
			||||||
 | 
					          <table className="List--table">
 | 
				
			||||||
 | 
					            <THead columns={selectedColumns} sortBy={sortBy} />
 | 
				
			||||||
 | 
					            <tbody>
 | 
				
			||||||
 | 
					              <tr className="List-padding" style={{ height: `${top}px` }} />
 | 
				
			||||||
 | 
					              {nodes.map((node, i) => (
 | 
				
			||||||
 | 
					                <Row
 | 
				
			||||||
 | 
					                  key={keys[i]}
 | 
				
			||||||
 | 
					                  node={node}
 | 
				
			||||||
 | 
					                  pins={pins}
 | 
				
			||||||
 | 
					                  columns={selectedColumns}
 | 
				
			||||||
 | 
					                />
 | 
				
			||||||
 | 
					              ))}
 | 
				
			||||||
 | 
					            </tbody>
 | 
				
			||||||
 | 
					          </table>
 | 
				
			||||||
 | 
					        </div>
 | 
				
			||||||
 | 
					        <Filter onChange={this.onFilterChange} />
 | 
				
			||||||
 | 
					      </div>
 | 
				
			||||||
 | 
					    );
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  // Get an array of keys for each `Node` in viewport in order.
 | 
				
			||||||
 | 
					  //
 | 
				
			||||||
 | 
					  // * If a `Node` was previously rendered, it will keep its `Key`.
 | 
				
			||||||
 | 
					  //
 | 
				
			||||||
 | 
					  // * If a `Node` is new to the viewport, it will get a `Key` of
 | 
				
			||||||
 | 
					  //   another `Node` that was removed from the viewport, or a new one.
 | 
				
			||||||
 | 
					  private recalculateKeys(nodes: Array<Node>): Array<Key> {
 | 
				
			||||||
 | 
					    // First we find all keys for `Node`s which didn't change from
 | 
				
			||||||
 | 
					    // last render.
 | 
				
			||||||
 | 
					    const keptKeys: Array<Maybe<Key>> = nodes.map(({ id }) => {
 | 
				
			||||||
 | 
					      const key = this.previousKeys.get(id);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      if (key != null) {
 | 
				
			||||||
 | 
					        this.previousKeys.delete(id);
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      return key;
 | 
				
			||||||
 | 
					    });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    // Array of all unused keys
 | 
				
			||||||
 | 
					    const unusedKeys = Array.from(this.previousKeys.values());
 | 
				
			||||||
 | 
					    let search = 0;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    // Clear the map so we can set new values
 | 
				
			||||||
 | 
					    this.previousKeys.clear();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    // Filling in blanks and re-populate previousKeys
 | 
				
			||||||
 | 
					    return keptKeys.map((key: Maybe<Key>, i) => {
 | 
				
			||||||
 | 
					      const id = nodes[i].id;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      // `Node` was previously in viewport
 | 
				
			||||||
 | 
					      if (key != null) {
 | 
				
			||||||
 | 
					        this.previousKeys.set(id, key);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        return key;
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      // Recycle the next unused key
 | 
				
			||||||
 | 
					      if (search < unusedKeys.length) {
 | 
				
			||||||
 | 
					        const unused = unusedKeys[search++];
 | 
				
			||||||
 | 
					        this.previousKeys.set(id, unused);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        return unused;
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      // No unused keys left, generate a new key
 | 
				
			||||||
 | 
					      const newKey = this.nextKey++;
 | 
				
			||||||
 | 
					      this.previousKeys.set(id, newKey);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      return newKey;
 | 
				
			||||||
 | 
					    });
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  private onScroll = () => {
 | 
				
			||||||
 | 
					    const relativeTop = divisibleBy(
 | 
				
			||||||
 | 
					      window.scrollY - (HEADER + TR_HEIGHT),
 | 
				
			||||||
 | 
					      TR_HEIGHT * ROW_MARGIN
 | 
				
			||||||
 | 
					    );
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    if (this.relativeTop === relativeTop) {
 | 
				
			||||||
 | 
					      return;
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    this.relativeTop = relativeTop;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    const { viewportHeight } = this.state;
 | 
				
			||||||
 | 
					    const top = Math.max(relativeTop, 0);
 | 
				
			||||||
 | 
					    const height =
 | 
				
			||||||
 | 
					      relativeTop < 0 ? viewportHeight + relativeTop : viewportHeight;
 | 
				
			||||||
 | 
					    const listStart = Math.max(((top / TR_HEIGHT) | 0) - ROW_MARGIN, 0);
 | 
				
			||||||
 | 
					    const listEnd = listStart + ROW_MARGIN * 2 + Math.ceil(height / TR_HEIGHT);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    if (listStart !== this.listStart || listEnd !== this.listEnd) {
 | 
				
			||||||
 | 
					      this.listStart = listStart;
 | 
				
			||||||
 | 
					      this.listEnd = listEnd;
 | 
				
			||||||
 | 
					      this.props.appUpdate({});
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					  };
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  private onResize = () => {
 | 
				
			||||||
 | 
					    const viewportHeight = viewport().height;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    this.setState({ viewportHeight });
 | 
				
			||||||
 | 
					  };
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  private onFilterChange = (filter: Maybe<(node: Node) => boolean>) => {
 | 
				
			||||||
 | 
					    this.setState({ filter });
 | 
				
			||||||
 | 
					  };
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					function divisibleBy(n: number, dividor: number): number {
 | 
				
			||||||
 | 
					  return n - (n % dividor);
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
							
								
								
									
										27
									
								
								src/components/List/Row.css
									
									
									
									
									
										Normal file
									
								
							
							
						
						@ -0,0 +1,27 @@
 | 
				
			|||||||
 | 
					.Row {
 | 
				
			||||||
 | 
					  color: #fff;
 | 
				
			||||||
 | 
					  cursor: pointer;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					.Row-pinned td:first-child {
 | 
				
			||||||
 | 
					  border-left: 5px solid #F2E370;
 | 
				
			||||||
 | 
					  padding-left: 10px;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					.Row-pinned td:last-child {
 | 
				
			||||||
 | 
					  border-right: 5px solid #F2E370;
 | 
				
			||||||
 | 
					  padding-right: 10px;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					.Row-pinned {
 | 
				
			||||||
 | 
					  color: #f2e370 !important;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					.Row-stale {
 | 
				
			||||||
 | 
					  color: #fff;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					.Row:hover {
 | 
				
			||||||
 | 
					  background-color: #1b3563;
 | 
				
			||||||
 | 
					  color: #fff;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
							
								
								
									
										116
									
								
								src/components/List/Row.tsx
									
									
									
									
									
										Normal file
									
								
							
							
						
						@ -0,0 +1,116 @@
 | 
				
			|||||||
 | 
					import * as React from 'react';
 | 
				
			||||||
 | 
					import { Types } from '../../common';
 | 
				
			||||||
 | 
					import { Node } from '../../state';
 | 
				
			||||||
 | 
					import { PersistentSet } from '../../persist';
 | 
				
			||||||
 | 
					import {
 | 
				
			||||||
 | 
					  Column,
 | 
				
			||||||
 | 
					  NameColumn,
 | 
				
			||||||
 | 
					  LocationColumn,
 | 
				
			||||||
 | 
					  NetworkIdColumn,
 | 
				
			||||||
 | 
					  PeersColumn,
 | 
				
			||||||
 | 
					  TxsColumn,
 | 
				
			||||||
 | 
					  UploadColumn,
 | 
				
			||||||
 | 
					  DownloadColumn,
 | 
				
			||||||
 | 
					  StateCacheColumn,
 | 
				
			||||||
 | 
					  BlockNumberColumn,
 | 
				
			||||||
 | 
					  BlockHashColumn,
 | 
				
			||||||
 | 
					  FinalizedBlockColumn,
 | 
				
			||||||
 | 
					  FinalizedHashColumn,
 | 
				
			||||||
 | 
					  BlockTimeColumn,
 | 
				
			||||||
 | 
					  BlockPropagationColumn,
 | 
				
			||||||
 | 
					  LastBlockColumn,
 | 
				
			||||||
 | 
					  UptimeColumn,
 | 
				
			||||||
 | 
					  CpuArchitectureColumn,
 | 
				
			||||||
 | 
					  CpuColumn,
 | 
				
			||||||
 | 
					  CpuCoresColumn,
 | 
				
			||||||
 | 
					  MemoryColumn,
 | 
				
			||||||
 | 
					  OperatingSystemColumn,
 | 
				
			||||||
 | 
					  VersionColumn,
 | 
				
			||||||
 | 
					  IsVirtualMachineColumn,
 | 
				
			||||||
 | 
					  LinuxDistroColumn,
 | 
				
			||||||
 | 
					  LinuxKernelColumn,
 | 
				
			||||||
 | 
					} from './';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					import './Row.css';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					interface RowProps {
 | 
				
			||||||
 | 
					  node: Node;
 | 
				
			||||||
 | 
					  pins: PersistentSet<Types.NodeName>;
 | 
				
			||||||
 | 
					  columns: Column[];
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					interface RowState {
 | 
				
			||||||
 | 
					  update: number;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export class Row extends React.Component<RowProps, RowState> {
 | 
				
			||||||
 | 
					  public static readonly columns: Column[] = [
 | 
				
			||||||
 | 
					    NameColumn,
 | 
				
			||||||
 | 
					    LocationColumn,
 | 
				
			||||||
 | 
					    NetworkIdColumn,
 | 
				
			||||||
 | 
					    PeersColumn,
 | 
				
			||||||
 | 
					    TxsColumn,
 | 
				
			||||||
 | 
					    UploadColumn,
 | 
				
			||||||
 | 
					    DownloadColumn,
 | 
				
			||||||
 | 
					    StateCacheColumn,
 | 
				
			||||||
 | 
					    BlockNumberColumn,
 | 
				
			||||||
 | 
					    BlockHashColumn,
 | 
				
			||||||
 | 
					    FinalizedBlockColumn,
 | 
				
			||||||
 | 
					    FinalizedHashColumn,
 | 
				
			||||||
 | 
					    BlockTimeColumn,
 | 
				
			||||||
 | 
					    BlockPropagationColumn,
 | 
				
			||||||
 | 
					    LastBlockColumn,
 | 
				
			||||||
 | 
					    UptimeColumn,
 | 
				
			||||||
 | 
					    VersionColumn,
 | 
				
			||||||
 | 
					    OperatingSystemColumn,
 | 
				
			||||||
 | 
					    CpuArchitectureColumn,
 | 
				
			||||||
 | 
					    CpuColumn,
 | 
				
			||||||
 | 
					    CpuCoresColumn,
 | 
				
			||||||
 | 
					    MemoryColumn,
 | 
				
			||||||
 | 
					    LinuxDistroColumn,
 | 
				
			||||||
 | 
					    LinuxKernelColumn,
 | 
				
			||||||
 | 
					    IsVirtualMachineColumn,
 | 
				
			||||||
 | 
					  ];
 | 
				
			||||||
 | 
					  private renderedChangeRef = 0;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  public shouldComponentUpdate(nextProps: RowProps): boolean {
 | 
				
			||||||
 | 
					    return (
 | 
				
			||||||
 | 
					      this.props.node.id !== nextProps.node.id ||
 | 
				
			||||||
 | 
					      this.renderedChangeRef !== nextProps.node.changeRef
 | 
				
			||||||
 | 
					    );
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  public render() {
 | 
				
			||||||
 | 
					    const { node, columns } = this.props;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    this.renderedChangeRef = node.changeRef;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    let className = 'Row';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    if (node.pinned) {
 | 
				
			||||||
 | 
					      className += ' Row-pinned';
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    if (node.stale) {
 | 
				
			||||||
 | 
					      className += ' Row-stale';
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    return (
 | 
				
			||||||
 | 
					      <tr className={className} onClick={this.toggle}>
 | 
				
			||||||
 | 
					        {columns.map((col, index) =>
 | 
				
			||||||
 | 
					          React.createElement(col, { node, key: index })
 | 
				
			||||||
 | 
					        )}
 | 
				
			||||||
 | 
					      </tr>
 | 
				
			||||||
 | 
					    );
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  public toggle = () => {
 | 
				
			||||||
 | 
					    const { pins, node } = this.props;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    if (node.pinned) {
 | 
				
			||||||
 | 
					      pins.delete(node.name);
 | 
				
			||||||
 | 
					    } else {
 | 
				
			||||||
 | 
					      pins.add(node.name);
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					  };
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
							
								
								
									
										24
									
								
								src/components/List/THead.css
									
									
									
									
									
										Normal file
									
								
							
							
						
						@ -0,0 +1,24 @@
 | 
				
			|||||||
 | 
					.THead {
 | 
				
			||||||
 | 
					  background: #50759e;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					.THeadCell {
 | 
				
			||||||
 | 
					  text-align: left;
 | 
				
			||||||
 | 
					  padding: 8px 13px;
 | 
				
			||||||
 | 
					  height: 23px;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					.THeadCell-sortable {
 | 
				
			||||||
 | 
					  cursor: pointer;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					.THeadCell-sorted {
 | 
				
			||||||
 | 
					  cursor: pointer;
 | 
				
			||||||
 | 
					  background: #345b87;
 | 
				
			||||||
 | 
					  color: #fff;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					.THeadCell-container {
 | 
				
			||||||
 | 
					  position: relative;
 | 
				
			||||||
 | 
					  display: inline-block;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
							
								
								
									
										48
									
								
								src/components/List/THead.tsx
									
									
									
									
									
										Normal file
									
								
							
							
						
						@ -0,0 +1,48 @@
 | 
				
			|||||||
 | 
					import * as React from 'react';
 | 
				
			||||||
 | 
					import { Maybe } from '../../common';
 | 
				
			||||||
 | 
					import { Column, THeadCell } from './';
 | 
				
			||||||
 | 
					import { Persistent } from '../../persist';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					import './THead.css';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					interface THeadProps {
 | 
				
			||||||
 | 
					  columns: Column[];
 | 
				
			||||||
 | 
					  sortBy: Persistent<Maybe<number>>;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export class THead extends React.Component<THeadProps> {
 | 
				
			||||||
 | 
					  private sortBy: Maybe<number>;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  constructor(props: THeadProps) {
 | 
				
			||||||
 | 
					    super(props);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    this.sortBy = props.sortBy.get();
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  public shouldComponentUpdate(nextProps: THeadProps) {
 | 
				
			||||||
 | 
					    return this.sortBy !== nextProps.sortBy.get();
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  public render() {
 | 
				
			||||||
 | 
					    const { columns, sortBy } = this.props;
 | 
				
			||||||
 | 
					    const last = columns.length - 1;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    this.sortBy = sortBy.get();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    return (
 | 
				
			||||||
 | 
					      <thead>
 | 
				
			||||||
 | 
					        <tr className="THead">
 | 
				
			||||||
 | 
					          {columns.map((col, index) => (
 | 
				
			||||||
 | 
					            <THeadCell
 | 
				
			||||||
 | 
					              key={index}
 | 
				
			||||||
 | 
					              column={col}
 | 
				
			||||||
 | 
					              index={index}
 | 
				
			||||||
 | 
					              last={last}
 | 
				
			||||||
 | 
					              sortBy={sortBy}
 | 
				
			||||||
 | 
					            />
 | 
				
			||||||
 | 
					          ))}
 | 
				
			||||||
 | 
					        </tr>
 | 
				
			||||||
 | 
					      </thead>
 | 
				
			||||||
 | 
					    );
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
							
								
								
									
										63
									
								
								src/components/List/THeadCell.tsx
									
									
									
									
									
										Normal file
									
								
							
							
						
						@ -0,0 +1,63 @@
 | 
				
			|||||||
 | 
					import * as React from 'react';
 | 
				
			||||||
 | 
					import { Maybe } from '../../common';
 | 
				
			||||||
 | 
					import { Column } from './';
 | 
				
			||||||
 | 
					import { Icon, Tooltip } from '../';
 | 
				
			||||||
 | 
					import { Persistent } from '../../persist';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					import sortAscIcon from '../../icons/triangle-up.svg';
 | 
				
			||||||
 | 
					import sortDescIcon from '../../icons/triangle-down.svg';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					interface THeadCellProps {
 | 
				
			||||||
 | 
					  column: Column;
 | 
				
			||||||
 | 
					  index: number;
 | 
				
			||||||
 | 
					  last: number;
 | 
				
			||||||
 | 
					  sortBy: Persistent<Maybe<number>>;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export class THeadCell extends React.Component<THeadCellProps> {
 | 
				
			||||||
 | 
					  public render() {
 | 
				
			||||||
 | 
					    const { column, index, last } = this.props;
 | 
				
			||||||
 | 
					    const { icon, width, label } = column;
 | 
				
			||||||
 | 
					    const position = index === 0 ? 'left' : index === last ? 'right' : 'center';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    const sortBy = this.props.sortBy.get();
 | 
				
			||||||
 | 
					    const className =
 | 
				
			||||||
 | 
					      column.sortBy == null
 | 
				
			||||||
 | 
					        ? 'THeadCell'
 | 
				
			||||||
 | 
					        : sortBy === index || sortBy === ~index
 | 
				
			||||||
 | 
					        ? 'THeadCell THeadCell-sorted'
 | 
				
			||||||
 | 
					        : 'THeadCell THeadCell-sortable';
 | 
				
			||||||
 | 
					    const i =
 | 
				
			||||||
 | 
					      sortBy === index ? sortAscIcon : sortBy === ~index ? sortDescIcon : icon;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    return (
 | 
				
			||||||
 | 
					      <th
 | 
				
			||||||
 | 
					        className={className}
 | 
				
			||||||
 | 
					        style={width ? { width } : undefined}
 | 
				
			||||||
 | 
					        onClick={this.toggleSort}
 | 
				
			||||||
 | 
					      >
 | 
				
			||||||
 | 
					        <span className="THeadCell-container">
 | 
				
			||||||
 | 
					          <Tooltip text={label} position={position} />
 | 
				
			||||||
 | 
					          <Icon src={i} />
 | 
				
			||||||
 | 
					        </span>
 | 
				
			||||||
 | 
					      </th>
 | 
				
			||||||
 | 
					    );
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  private toggleSort = () => {
 | 
				
			||||||
 | 
					    const { index, sortBy, column } = this.props;
 | 
				
			||||||
 | 
					    const sortByRaw = sortBy.get();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    if (column.sortBy == null) {
 | 
				
			||||||
 | 
					      return;
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    if (sortByRaw === index) {
 | 
				
			||||||
 | 
					      sortBy.set(~index);
 | 
				
			||||||
 | 
					    } else if (sortByRaw === ~index) {
 | 
				
			||||||
 | 
					      sortBy.set(null);
 | 
				
			||||||
 | 
					    } else {
 | 
				
			||||||
 | 
					      sortBy.set(index);
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					  };
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
							
								
								
									
										5
									
								
								src/components/List/index.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						@ -0,0 +1,5 @@
 | 
				
			|||||||
 | 
					export * from './Column';
 | 
				
			||||||
 | 
					export * from './List';
 | 
				
			||||||
 | 
					export * from './Row';
 | 
				
			||||||
 | 
					export * from './THeadCell';
 | 
				
			||||||
 | 
					export * from './THead';
 | 
				
			||||||
							
								
								
									
										156
									
								
								src/components/Map/Location.css
									
									
									
									
									
										Normal file
									
								
							
							
						
						@ -0,0 +1,156 @@
 | 
				
			|||||||
 | 
					.Location, .Validator-Location {
 | 
				
			||||||
 | 
					  width: 6px;
 | 
				
			||||||
 | 
					  height: 6px;
 | 
				
			||||||
 | 
					  background: transparent;
 | 
				
			||||||
 | 
					  border: 2px solid #f2e370;
 | 
				
			||||||
 | 
					  border-radius: 6px;
 | 
				
			||||||
 | 
					  margin-left: -4px;
 | 
				
			||||||
 | 
					  margin-top: -4px;
 | 
				
			||||||
 | 
					  position: absolute;
 | 
				
			||||||
 | 
					  top: 50%;
 | 
				
			||||||
 | 
					  left: 50%;
 | 
				
			||||||
 | 
					  cursor: pointer;
 | 
				
			||||||
 | 
					  z-index: 2;
 | 
				
			||||||
 | 
					  transition: border-color 0.25s linear,
 | 
				
			||||||
 | 
					              width 0.25s linear,
 | 
				
			||||||
 | 
					              height 0.25s linear,
 | 
				
			||||||
 | 
					              background 0.25s linear;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					.Validator-Location {
 | 
				
			||||||
 | 
					  border: 2px solid #60c45b;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					.Location-dimmed {
 | 
				
			||||||
 | 
					  z-index: 1;
 | 
				
			||||||
 | 
					  border-color: #fff8c5;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					.Location-dimmed-validator {
 | 
				
			||||||
 | 
					  z-index: 1;
 | 
				
			||||||
 | 
					  border-color: #b9ecb6;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					.Location-ping {
 | 
				
			||||||
 | 
					  pointer-events: none;
 | 
				
			||||||
 | 
					  position: absolute;
 | 
				
			||||||
 | 
					  display: none;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					.Location-synced {
 | 
				
			||||||
 | 
					  z-index: 3;
 | 
				
			||||||
 | 
					  border-color: #f2e370;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					.Location-synced-validator {
 | 
				
			||||||
 | 
					  z-index: 3;
 | 
				
			||||||
 | 
					  border-color: #60C45B;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					.Location-synced .Location-ping {
 | 
				
			||||||
 | 
					  border: 1px solid #f2e370;
 | 
				
			||||||
 | 
					  border-radius: 30px;
 | 
				
			||||||
 | 
					  display: block;
 | 
				
			||||||
 | 
					  animation: ping 1s forwards;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					.Location-synced-validator .Location-ping {
 | 
				
			||||||
 | 
					  border: 1px solid #60C45B;
 | 
				
			||||||
 | 
					  border-radius: 30px;
 | 
				
			||||||
 | 
					  display: block;
 | 
				
			||||||
 | 
					  animation: ping-validator 1s forwards;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					.Location:hover, .Validator-Location:hover {
 | 
				
			||||||
 | 
					  z-index: 4;
 | 
				
			||||||
 | 
					  border-color: #fff;
 | 
				
			||||||
 | 
					  width: 7px;
 | 
				
			||||||
 | 
					  height: 7px;
 | 
				
			||||||
 | 
					  background: #fff;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					.Location-details {
 | 
				
			||||||
 | 
					  min-width: 335px;
 | 
				
			||||||
 | 
					  position: absolute;
 | 
				
			||||||
 | 
					  font-family: 'Ubuntu', sans-serif;
 | 
				
			||||||
 | 
					  background: #50759e;
 | 
				
			||||||
 | 
					  border: 1px solid #fff;
 | 
				
			||||||
 | 
					  border-radius: 5px;
 | 
				
			||||||
 | 
					  color: #fff;
 | 
				
			||||||
 | 
					  box-shadow: 0 3px 20px rgba(0, 0, 0, 0.5);
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					.Location-quarter0 .Location-details {
 | 
				
			||||||
 | 
					  left: 16px;
 | 
				
			||||||
 | 
					  top: -4px;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					.Location-quarter1 .Location-details {
 | 
				
			||||||
 | 
					  right: 16px;
 | 
				
			||||||
 | 
					  top: -4px;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					.Location-quarter2 .Location-details {
 | 
				
			||||||
 | 
					  left: 16px;
 | 
				
			||||||
 | 
					  bottom: -4px;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					.Location-quarter3 .Location-details {
 | 
				
			||||||
 | 
					  right: 16px;
 | 
				
			||||||
 | 
					  bottom: -4px;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					.Location-details td {
 | 
				
			||||||
 | 
					  text-align: left;
 | 
				
			||||||
 | 
					  padding: 0.5em 1em;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					.Location-details td:nth-child(odd) {
 | 
				
			||||||
 | 
					  width: 16px;
 | 
				
			||||||
 | 
					  text-align: center;
 | 
				
			||||||
 | 
					  padding-right: 0.2em;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					.Location-details td:nth-child(even) {
 | 
				
			||||||
 | 
					  padding-left: 0.2em;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					@keyframes ping {
 | 
				
			||||||
 | 
					  from {
 | 
				
			||||||
 | 
					    left: -1px;
 | 
				
			||||||
 | 
					    top: -1px;
 | 
				
			||||||
 | 
					    width: 6px;
 | 
				
			||||||
 | 
					    height: 6px;
 | 
				
			||||||
 | 
					    border-width: 1px;
 | 
				
			||||||
 | 
					    border-color: rgba(242, 227, 112, 1);
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  to {
 | 
				
			||||||
 | 
					    left: -18px;
 | 
				
			||||||
 | 
					    top: -18px;
 | 
				
			||||||
 | 
					    width: 40px;
 | 
				
			||||||
 | 
					    height: 40px;
 | 
				
			||||||
 | 
					    border-width: 1px;
 | 
				
			||||||
 | 
					    border-color: rgba(242, 227, 112, 0);
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					@keyframes ping-validator {
 | 
				
			||||||
 | 
					  from {
 | 
				
			||||||
 | 
					    left: -1px;
 | 
				
			||||||
 | 
					    top: -1px;
 | 
				
			||||||
 | 
					    width: 6px;
 | 
				
			||||||
 | 
					    height: 6px;
 | 
				
			||||||
 | 
					    border-width: 1px;
 | 
				
			||||||
 | 
					    border-color: rgba(96, 196, 91, 1);
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  to {
 | 
				
			||||||
 | 
					    left: -18px;
 | 
				
			||||||
 | 
					    top: -18px;
 | 
				
			||||||
 | 
					    width: 40px;
 | 
				
			||||||
 | 
					    height: 40px;
 | 
				
			||||||
 | 
					    border-width: 1px;
 | 
				
			||||||
 | 
					    border-color: rgba(96, 196, 91, 0);
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
							
								
								
									
										167
									
								
								src/components/Map/Location.tsx
									
									
									
									
									
										Normal file
									
								
							
							
						
						@ -0,0 +1,167 @@
 | 
				
			|||||||
 | 
					import * as React from 'react';
 | 
				
			||||||
 | 
					import {
 | 
				
			||||||
 | 
					  formatNumber,
 | 
				
			||||||
 | 
					  trimHash,
 | 
				
			||||||
 | 
					  milliOrSecond,
 | 
				
			||||||
 | 
					  secondsWithPrecision,
 | 
				
			||||||
 | 
					} from '../../utils';
 | 
				
			||||||
 | 
					import { Ago, Icon } from '../';
 | 
				
			||||||
 | 
					import { Node } from '../../state';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					import nodeIcon from '../../icons/server.svg';
 | 
				
			||||||
 | 
					import nodeTypeIcon from '../../icons/terminal.svg';
 | 
				
			||||||
 | 
					import nodeLocationIcon from '../../icons/location.svg';
 | 
				
			||||||
 | 
					import blockIcon from '../../icons/package.svg';
 | 
				
			||||||
 | 
					import blockHashIcon from '../../icons/file-binary.svg';
 | 
				
			||||||
 | 
					import blockTimeIcon from '../../icons/history.svg';
 | 
				
			||||||
 | 
					import propagationTimeIcon from '../../icons/dashboard.svg';
 | 
				
			||||||
 | 
					import lastTimeIcon from '../../icons/watch.svg';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					import './Location.css';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export type LocationQuarter = 0 | 1 | 2 | 3;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export interface LocationOffsets {
 | 
				
			||||||
 | 
					  leftOffset: number;
 | 
				
			||||||
 | 
					  topOffset: number;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					interface LocationProps {
 | 
				
			||||||
 | 
					  node: Node;
 | 
				
			||||||
 | 
					  position: LocationPosition;
 | 
				
			||||||
 | 
					  focused: boolean;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export interface LocationPosition {
 | 
				
			||||||
 | 
					  left: number;
 | 
				
			||||||
 | 
					  top: number;
 | 
				
			||||||
 | 
					  quarter: LocationQuarter;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					interface LocationState {
 | 
				
			||||||
 | 
					  hover: boolean;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export class Location extends React.Component<LocationProps, LocationState> {
 | 
				
			||||||
 | 
					  public readonly state = { hover: false };
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  public render() {
 | 
				
			||||||
 | 
					    const { node, position, focused } = this.props;
 | 
				
			||||||
 | 
					    const { left, top, quarter } = position;
 | 
				
			||||||
 | 
					    const { height, propagationTime, city } = node;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    if (!city) {
 | 
				
			||||||
 | 
					      return null;
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    let className = `Location Location-quarter${quarter}`;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    if (focused) {
 | 
				
			||||||
 | 
					      if (propagationTime != null) {
 | 
				
			||||||
 | 
					        className += ' Location-synced';
 | 
				
			||||||
 | 
					      } else if (height % 2 === 1) {
 | 
				
			||||||
 | 
					        className += ' Location-odd';
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					    } else {
 | 
				
			||||||
 | 
					      className += ' Location-dimmed';
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    if (node.validator) {
 | 
				
			||||||
 | 
					      className = `Validator-${className}-validator`;
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    return (
 | 
				
			||||||
 | 
					      <div
 | 
				
			||||||
 | 
					        className={className}
 | 
				
			||||||
 | 
					        style={{ left, top }}
 | 
				
			||||||
 | 
					        onMouseOver={this.onMouseOver}
 | 
				
			||||||
 | 
					        onMouseOut={this.onMouseOut}
 | 
				
			||||||
 | 
					      >
 | 
				
			||||||
 | 
					        {this.state.hover ? this.renderDetails() : null}
 | 
				
			||||||
 | 
					        <div className="Location-ping" />
 | 
				
			||||||
 | 
					      </div>
 | 
				
			||||||
 | 
					    );
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  private renderDetails() {
 | 
				
			||||||
 | 
					    const {
 | 
				
			||||||
 | 
					      name,
 | 
				
			||||||
 | 
					      implementation,
 | 
				
			||||||
 | 
					      version,
 | 
				
			||||||
 | 
					      validator,
 | 
				
			||||||
 | 
					      height,
 | 
				
			||||||
 | 
					      hash,
 | 
				
			||||||
 | 
					      blockTime,
 | 
				
			||||||
 | 
					      blockTimestamp,
 | 
				
			||||||
 | 
					      propagationTime,
 | 
				
			||||||
 | 
					      city,
 | 
				
			||||||
 | 
					    } = this.props.node;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    return (
 | 
				
			||||||
 | 
					      <table className="Location-details Location-details">
 | 
				
			||||||
 | 
					        <tbody>
 | 
				
			||||||
 | 
					          <tr>
 | 
				
			||||||
 | 
					            <td title="Node">
 | 
				
			||||||
 | 
					              <Icon src={nodeIcon} />
 | 
				
			||||||
 | 
					            </td>
 | 
				
			||||||
 | 
					            <td colSpan={5}>{name}</td>
 | 
				
			||||||
 | 
					          </tr>
 | 
				
			||||||
 | 
					          <tr>
 | 
				
			||||||
 | 
					            <td title="Implementation">
 | 
				
			||||||
 | 
					              <Icon src={nodeTypeIcon} />
 | 
				
			||||||
 | 
					            </td>
 | 
				
			||||||
 | 
					            <td colSpan={5}>
 | 
				
			||||||
 | 
					              {implementation} v{version}
 | 
				
			||||||
 | 
					            </td>
 | 
				
			||||||
 | 
					          </tr>
 | 
				
			||||||
 | 
					          <tr>
 | 
				
			||||||
 | 
					            <td title="Location">
 | 
				
			||||||
 | 
					              <Icon src={nodeLocationIcon} />
 | 
				
			||||||
 | 
					            </td>
 | 
				
			||||||
 | 
					            <td colSpan={5}>{city}</td>
 | 
				
			||||||
 | 
					          </tr>
 | 
				
			||||||
 | 
					          <tr>
 | 
				
			||||||
 | 
					            <td title="Block">
 | 
				
			||||||
 | 
					              <Icon src={blockIcon} />
 | 
				
			||||||
 | 
					            </td>
 | 
				
			||||||
 | 
					            <td colSpan={5}>#{formatNumber(height)}</td>
 | 
				
			||||||
 | 
					          </tr>
 | 
				
			||||||
 | 
					          <tr>
 | 
				
			||||||
 | 
					            <td title="Block Hash">
 | 
				
			||||||
 | 
					              <Icon src={blockHashIcon} />
 | 
				
			||||||
 | 
					            </td>
 | 
				
			||||||
 | 
					            <td colSpan={5}>{trimHash(hash, 20)}</td>
 | 
				
			||||||
 | 
					          </tr>
 | 
				
			||||||
 | 
					          <tr>
 | 
				
			||||||
 | 
					            <td title="Block Time">
 | 
				
			||||||
 | 
					              <Icon src={blockTimeIcon} />
 | 
				
			||||||
 | 
					            </td>
 | 
				
			||||||
 | 
					            <td style={{ width: 80 }}>
 | 
				
			||||||
 | 
					              {secondsWithPrecision(blockTime / 1000)}
 | 
				
			||||||
 | 
					            </td>
 | 
				
			||||||
 | 
					            <td title="Block Propagation Time">
 | 
				
			||||||
 | 
					              <Icon src={propagationTimeIcon} />
 | 
				
			||||||
 | 
					            </td>
 | 
				
			||||||
 | 
					            <td style={{ width: 58 }}>
 | 
				
			||||||
 | 
					              {propagationTime == null ? '∞' : milliOrSecond(propagationTime)}
 | 
				
			||||||
 | 
					            </td>
 | 
				
			||||||
 | 
					            <td title="Last Block Time">
 | 
				
			||||||
 | 
					              <Icon src={lastTimeIcon} />
 | 
				
			||||||
 | 
					            </td>
 | 
				
			||||||
 | 
					            <td style={{ minWidth: 82 }}>
 | 
				
			||||||
 | 
					              <Ago when={blockTimestamp} />
 | 
				
			||||||
 | 
					            </td>
 | 
				
			||||||
 | 
					          </tr>
 | 
				
			||||||
 | 
					        </tbody>
 | 
				
			||||||
 | 
					      </table>
 | 
				
			||||||
 | 
					    );
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  private onMouseOver = () => {
 | 
				
			||||||
 | 
					    this.setState({ hover: true });
 | 
				
			||||||
 | 
					  };
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  private onMouseOut = () => {
 | 
				
			||||||
 | 
					    this.setState({ hover: false });
 | 
				
			||||||
 | 
					  };
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
							
								
								
									
										18
									
								
								src/components/Map/Map.css
									
									
									
									
									
										Normal file
									
								
							
							
						
						@ -0,0 +1,18 @@
 | 
				
			|||||||
 | 
					.Map-container {
 | 
				
			||||||
 | 
					  background: linear-gradient(90deg, rgba(28,54,100,1) 0%, rgba(45,87,132,1) 100%);
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					.Map {
 | 
				
			||||||
 | 
					  background: url('../../assets/world-map.svg') no-repeat;
 | 
				
			||||||
 | 
					  background-size: contain;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					.Map-container, .Map {
 | 
				
			||||||
 | 
					  min-width: 1350px;
 | 
				
			||||||
 | 
					  background-position: center;
 | 
				
			||||||
 | 
					  position: absolute;
 | 
				
			||||||
 | 
					  left: 0;
 | 
				
			||||||
 | 
					  right: 0;
 | 
				
			||||||
 | 
					  top: 0;
 | 
				
			||||||
 | 
					  bottom: 0;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
							
								
								
									
										164
									
								
								src/components/Map/Map.tsx
									
									
									
									
									
										Normal file
									
								
							
							
						
						@ -0,0 +1,164 @@
 | 
				
			|||||||
 | 
					import * as React from 'react';
 | 
				
			||||||
 | 
					import ReactGA from "react-ga4";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					import { Types, Maybe } from '../../common';
 | 
				
			||||||
 | 
					import { Filter } from '../';
 | 
				
			||||||
 | 
					import { State as AppState, Node } from '../../state';
 | 
				
			||||||
 | 
					import { Location, LocationQuarter, LocationPosition, LocationOffsets } from './';
 | 
				
			||||||
 | 
					import { viewport } from '../../utils';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					const MAP_RATIO = 800 / 350;
 | 
				
			||||||
 | 
					const MAP_HEIGHT_ADJUST = 400 / 350;
 | 
				
			||||||
 | 
					const HEADER = 148;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					import './Map.css';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					interface MapProps {
 | 
				
			||||||
 | 
					  appState: Readonly<AppState>;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					interface MapState {
 | 
				
			||||||
 | 
					  filter: Maybe<(node: Node) => boolean>;
 | 
				
			||||||
 | 
					  width: number;
 | 
				
			||||||
 | 
					  height: number;
 | 
				
			||||||
 | 
					  top: number;
 | 
				
			||||||
 | 
					  left: number;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export class WorldMap extends React.Component<MapProps, MapState> {
 | 
				
			||||||
 | 
					  public state: MapState = {
 | 
				
			||||||
 | 
					    filter: null,
 | 
				
			||||||
 | 
					    width: 0,
 | 
				
			||||||
 | 
					    height: 0,
 | 
				
			||||||
 | 
					    top: 0,
 | 
				
			||||||
 | 
					    left: 0,
 | 
				
			||||||
 | 
					  };
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  public offsets: Map<Types.NetworkId, LocationOffsets> = new Map<Types.NetworkId, LocationOffsets>();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  public usedLeftPositions: Set<number> = new Set<number>();
 | 
				
			||||||
 | 
					  public usedTopPositions: Set<number> = new Set<number>();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  public componentDidMount() {
 | 
				
			||||||
 | 
					    ReactGA.send({ hitType: "pageview", page: "/map" });
 | 
				
			||||||
 | 
					    this.onResize();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    window.addEventListener('resize', this.onResize);
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  public componentWillUnmount() {
 | 
				
			||||||
 | 
					    window.removeEventListener('resize', this.onResize);
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  public render() {
 | 
				
			||||||
 | 
					    const { appState } = this.props;
 | 
				
			||||||
 | 
					    const { filter } = this.state;
 | 
				
			||||||
 | 
					    const nodes = appState.nodes.sorted();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    return (
 | 
				
			||||||
 | 
					      <div className="Map-container">
 | 
				
			||||||
 | 
					        <div className="Map">
 | 
				
			||||||
 | 
					          {nodes.map(node => {
 | 
				
			||||||
 | 
					            const { lat, lon } = node;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            const focused = filter == null || filter(node);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            if (lat == null || lon == null) {
 | 
				
			||||||
 | 
					              // Skip nodes with unknown location
 | 
				
			||||||
 | 
					              return null;
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            const position = this.pixelPosition(node.networkId, lat, lon);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            return (
 | 
				
			||||||
 | 
					              <Location
 | 
				
			||||||
 | 
					                key={node.id}
 | 
				
			||||||
 | 
					                position={position}
 | 
				
			||||||
 | 
					                focused={focused}
 | 
				
			||||||
 | 
					                node={node}
 | 
				
			||||||
 | 
					              />
 | 
				
			||||||
 | 
					            );
 | 
				
			||||||
 | 
					          })}
 | 
				
			||||||
 | 
					        </div>
 | 
				
			||||||
 | 
					        <Filter onChange={this.onFilterChange} />
 | 
				
			||||||
 | 
					      </div>
 | 
				
			||||||
 | 
					    );
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  private pixelPosition(
 | 
				
			||||||
 | 
					    id: Maybe<Types.NetworkId>,
 | 
				
			||||||
 | 
					    lat: Types.Latitude,
 | 
				
			||||||
 | 
					    lon: Types.Longitude,
 | 
				
			||||||
 | 
					  ): LocationPosition {
 | 
				
			||||||
 | 
					    const { state, offsets, usedLeftPositions, usedTopPositions } = this;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    // Longitude ranges -180 (west) to +180 (east)
 | 
				
			||||||
 | 
					    // Latitude ranges +90 (north) to -90 (south)
 | 
				
			||||||
 | 
					    let left = Math.round(((180 + lon) / 360) * state.width + state.left);
 | 
				
			||||||
 | 
					    let top = Math.round(
 | 
				
			||||||
 | 
					      ((90 - lat) / 180) * state.height * MAP_HEIGHT_ADJUST + state.top
 | 
				
			||||||
 | 
					    );
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    let quarter: LocationQuarter = 0;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    if (lon > 0) {
 | 
				
			||||||
 | 
					      quarter = (quarter | 1) as LocationQuarter;
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    if (lat < 0) {
 | 
				
			||||||
 | 
					      quarter = (quarter | 2) as LocationQuarter;
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    let topOffset = 0;
 | 
				
			||||||
 | 
					    let leftOffset = 0;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    id = id!;
 | 
				
			||||||
 | 
					    const maybeOffsets = offsets.get(id);
 | 
				
			||||||
 | 
					    if (maybeOffsets) {
 | 
				
			||||||
 | 
					      topOffset = maybeOffsets.topOffset;
 | 
				
			||||||
 | 
					      leftOffset = maybeOffsets.leftOffset;
 | 
				
			||||||
 | 
					    } else if (usedLeftPositions.has(left) && usedTopPositions.has(top)) {
 | 
				
			||||||
 | 
					        topOffset = Math.floor(Math.random() * 12) - 6;
 | 
				
			||||||
 | 
					        leftOffset = Math.floor(Math.random() * 12) - 6;
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    left += leftOffset;
 | 
				
			||||||
 | 
					    top += topOffset;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    offsets.set(id, { leftOffset, topOffset });
 | 
				
			||||||
 | 
					    usedLeftPositions.add(left);
 | 
				
			||||||
 | 
					    usedTopPositions.add(top);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    return { left, top, quarter };
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  private onResize: () => void = () => {
 | 
				
			||||||
 | 
					    const vp = viewport();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    vp.width = Math.max(1350, vp.width);
 | 
				
			||||||
 | 
					    vp.height -= HEADER;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    const ratio = vp.width / vp.height;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    let top = 0;
 | 
				
			||||||
 | 
					    let left = 0;
 | 
				
			||||||
 | 
					    let width = 0;
 | 
				
			||||||
 | 
					    let height = 0;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    if (ratio >= MAP_RATIO) {
 | 
				
			||||||
 | 
					      width = Math.round(vp.height * MAP_RATIO);
 | 
				
			||||||
 | 
					      height = Math.round(vp.height);
 | 
				
			||||||
 | 
					      left = (vp.width - width) / 2;
 | 
				
			||||||
 | 
					    } else {
 | 
				
			||||||
 | 
					      width = Math.round(vp.width);
 | 
				
			||||||
 | 
					      height = Math.round(vp.width / MAP_RATIO);
 | 
				
			||||||
 | 
					      top = (vp.height - height) / 2;
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    this.setState({ top, left, width, height });
 | 
				
			||||||
 | 
					  };
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  private onFilterChange = (filter: Maybe<(node: Node) => boolean>) => {
 | 
				
			||||||
 | 
					    this.setState({ filter });
 | 
				
			||||||
 | 
					  };
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
							
								
								
									
										2
									
								
								src/components/Map/index.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						@ -0,0 +1,2 @@
 | 
				
			|||||||
 | 
					export * from './Map';
 | 
				
			||||||
 | 
					export * from './Location';
 | 
				
			||||||
							
								
								
									
										18
									
								
								src/components/Sparkline.css
									
									
									
									
									
										Normal file
									
								
							
							
						
						@ -0,0 +1,18 @@
 | 
				
			|||||||
 | 
					.Sparkline {
 | 
				
			||||||
 | 
					  fill: currentcolor;
 | 
				
			||||||
 | 
					  fill-opacity: 0.35;
 | 
				
			||||||
 | 
					  stroke: currentcolor;
 | 
				
			||||||
 | 
					  margin: 0 -1px -3px -1px;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					.Sparkline path {
 | 
				
			||||||
 | 
					  pointer-events: none;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					.Sparkline .Sparkline-cursor {
 | 
				
			||||||
 | 
					  display: none;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					.Sparkline:hover .Sparkline-cursor {
 | 
				
			||||||
 | 
					  display: initial;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
							
								
								
									
										101
									
								
								src/components/Sparkline.tsx
									
									
									
									
									
										Normal file
									
								
							
							
						
						@ -0,0 +1,101 @@
 | 
				
			|||||||
 | 
					import * as React from 'react';
 | 
				
			||||||
 | 
					import { Types, Maybe } from '../common';
 | 
				
			||||||
 | 
					import { Tooltip, TooltipUpdateCallback } from './';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					import './Sparkline.css';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					interface SparklineProps {
 | 
				
			||||||
 | 
					  stroke: number;
 | 
				
			||||||
 | 
					  width: number;
 | 
				
			||||||
 | 
					  height: number;
 | 
				
			||||||
 | 
					  values: number[];
 | 
				
			||||||
 | 
					  stamps?: Types.Timestamp[];
 | 
				
			||||||
 | 
					  minScale?: number;
 | 
				
			||||||
 | 
					  format?: (value: number, stamp: Maybe<Types.Timestamp>) => string;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export class Sparkline extends React.Component<SparklineProps> {
 | 
				
			||||||
 | 
					  private cursor: SVGPathElement;
 | 
				
			||||||
 | 
					  private update: TooltipUpdateCallback;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  public shouldComponentUpdate(nextProps: SparklineProps): boolean {
 | 
				
			||||||
 | 
					    const { stroke, width, height, format, values } = this.props;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    return (
 | 
				
			||||||
 | 
					      values !== nextProps.values ||
 | 
				
			||||||
 | 
					      stroke !== nextProps.stroke ||
 | 
				
			||||||
 | 
					      width !== nextProps.width ||
 | 
				
			||||||
 | 
					      height !== nextProps.height ||
 | 
				
			||||||
 | 
					      format !== nextProps.format
 | 
				
			||||||
 | 
					    );
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  public render() {
 | 
				
			||||||
 | 
					    const { stroke, width, height, minScale, values } = this.props;
 | 
				
			||||||
 | 
					    const padding = stroke / 2;
 | 
				
			||||||
 | 
					    const paddedHeight = height - padding;
 | 
				
			||||||
 | 
					    const paddedWidth = width - 2;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    const max = Math.max(minScale || 0, ...values);
 | 
				
			||||||
 | 
					    const offset = paddedWidth / (values.length - 1);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    let path = '';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    values.forEach((value, index) => {
 | 
				
			||||||
 | 
					      const x = 1 + index * offset;
 | 
				
			||||||
 | 
					      const y = padding + (1 - value / max) * paddedHeight;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      if (path) {
 | 
				
			||||||
 | 
					        path += ` L ${x} ${y}`;
 | 
				
			||||||
 | 
					      } else {
 | 
				
			||||||
 | 
					        path = `${x} ${y}`;
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					    });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    return (
 | 
				
			||||||
 | 
					      <>
 | 
				
			||||||
 | 
					        <Tooltip text="-" onInit={this.onTooltipInit} />
 | 
				
			||||||
 | 
					        <svg
 | 
				
			||||||
 | 
					          className="Sparkline"
 | 
				
			||||||
 | 
					          width={width}
 | 
				
			||||||
 | 
					          height={height}
 | 
				
			||||||
 | 
					          strokeWidth={stroke}
 | 
				
			||||||
 | 
					          onMouseMove={this.onMouseMove}
 | 
				
			||||||
 | 
					          onMouseLeave={this.onMouseLeave}
 | 
				
			||||||
 | 
					        >
 | 
				
			||||||
 | 
					          <path d={`M 0 ${height} L ${path} V ${height} Z`} stroke="none" />
 | 
				
			||||||
 | 
					          <path d={`M ${path}`} fill="none" />
 | 
				
			||||||
 | 
					          <path className="Sparkline-cursor" strokeWidth="2" ref={this.onRef} />
 | 
				
			||||||
 | 
					        </svg>
 | 
				
			||||||
 | 
					      </>
 | 
				
			||||||
 | 
					    );
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  private onRef = (cursor: SVGPathElement) => {
 | 
				
			||||||
 | 
					    this.cursor = cursor;
 | 
				
			||||||
 | 
					  };
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  private onTooltipInit = (update: TooltipUpdateCallback) => {
 | 
				
			||||||
 | 
					    this.update = update;
 | 
				
			||||||
 | 
					  };
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  private onMouseMove = (
 | 
				
			||||||
 | 
					    event: React.MouseEvent<SVGSVGElement, MouseEvent>
 | 
				
			||||||
 | 
					  ) => {
 | 
				
			||||||
 | 
					    const { width, height, values, format, stamps } = this.props;
 | 
				
			||||||
 | 
					    const offset = (width - 2) / (values.length - 1);
 | 
				
			||||||
 | 
					    const cur =
 | 
				
			||||||
 | 
					      Math.round((event.nativeEvent.offsetX - 1 - offset / 2) / offset) | 0;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    this.cursor.setAttribute('d', `M ${1 + offset * cur} 0 V ${height}`);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    const str = format
 | 
				
			||||||
 | 
					      ? format(values[cur], stamps ? stamps[cur] : null)
 | 
				
			||||||
 | 
					      : `${values[cur]}`;
 | 
				
			||||||
 | 
					    this.update(str);
 | 
				
			||||||
 | 
					  };
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  private onMouseLeave = () => {
 | 
				
			||||||
 | 
					    this.cursor.removeAttribute('d');
 | 
				
			||||||
 | 
					  };
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
							
								
								
									
										56
									
								
								src/components/Stats/Stats.css
									
									
									
									
									
										Normal file
									
								
							
							
						
						@ -0,0 +1,56 @@
 | 
				
			|||||||
 | 
					.Stats {
 | 
				
			||||||
 | 
					  text-align: center;
 | 
				
			||||||
 | 
					  padding-top: 2.5rem;
 | 
				
			||||||
 | 
					  padding-bottom: 0.1rem;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					.Stats-category {
 | 
				
			||||||
 | 
					  text-align: left;
 | 
				
			||||||
 | 
					  background-color: #345987;
 | 
				
			||||||
 | 
					  margin-bottom: 2.5rem;
 | 
				
			||||||
 | 
					  padding: 1rem;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					.Stats-category table {
 | 
				
			||||||
 | 
					  color: #fff;
 | 
				
			||||||
 | 
					  width: 100%;
 | 
				
			||||||
 | 
					  table-layout: fixed;
 | 
				
			||||||
 | 
					  border-collapse: collapse;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					.Stats-category tbody > tr:nth-child(odd) {
 | 
				
			||||||
 | 
					  background-color: #50759f;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					.Stats-percent {
 | 
				
			||||||
 | 
					  width: 6em;
 | 
				
			||||||
 | 
					  text-align: right;
 | 
				
			||||||
 | 
					  padding-left: 0.5rem;
 | 
				
			||||||
 | 
					  padding-right: 1rem;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					.Stats-count {
 | 
				
			||||||
 | 
					  width: 6.5em;
 | 
				
			||||||
 | 
					  text-align: right;
 | 
				
			||||||
 | 
					  padding-right: 1.5rem;
 | 
				
			||||||
 | 
					  border-right: 1px solid #fff;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					.Stats-value {
 | 
				
			||||||
 | 
					  padding-left: 2rem;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					th.Stats-value {
 | 
				
			||||||
 | 
					  padding-left: 1rem;
 | 
				
			||||||
 | 
					  padding-top: 0.5rem;
 | 
				
			||||||
 | 
					  padding-bottom: 0.5rem;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					.Stats-category td {
 | 
				
			||||||
 | 
					  padding-top: 0.5rem;
 | 
				
			||||||
 | 
					  padding-bottom: 0.5rem;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					.Stats-unknown {
 | 
				
			||||||
 | 
					  opacity: 0.5;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
							
								
								
									
										189
									
								
								src/components/Stats/Stats.tsx
									
									
									
									
									
										Normal file
									
								
							
							
						
						@ -0,0 +1,189 @@
 | 
				
			|||||||
 | 
					import * as React from 'react';
 | 
				
			||||||
 | 
					import ReactGA from "react-ga4";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					import { Maybe } from '../../common';
 | 
				
			||||||
 | 
					import { State as AppState } from '../../state';
 | 
				
			||||||
 | 
					import { Ranking, Range } from '../../common/types';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					import './Stats.css';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					interface StatsProps {
 | 
				
			||||||
 | 
					  appState: Readonly<AppState>;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					function displayPercentage(percent: number): string {
 | 
				
			||||||
 | 
					  return (Math.round(percent * 100) / 100).toFixed(2);
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					function generateRankingTable<T>(
 | 
				
			||||||
 | 
					  key: string,
 | 
				
			||||||
 | 
					  label: string,
 | 
				
			||||||
 | 
					  format: (value: T) => string,
 | 
				
			||||||
 | 
					  ranking: Ranking<T>
 | 
				
			||||||
 | 
					) {
 | 
				
			||||||
 | 
					  let total = ranking.other + ranking.unknown;
 | 
				
			||||||
 | 
					  ranking.list.forEach(([_, count]) => {
 | 
				
			||||||
 | 
					    total += count;
 | 
				
			||||||
 | 
					  });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  if (ranking.unknown === total) {
 | 
				
			||||||
 | 
					    return null;
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  const entries: React.ReactNode[] = [];
 | 
				
			||||||
 | 
					  ranking.list.forEach(([value, count]) => {
 | 
				
			||||||
 | 
					    const percent = displayPercentage((count / total) * 100);
 | 
				
			||||||
 | 
					    const index = entries.length;
 | 
				
			||||||
 | 
					    entries.push(
 | 
				
			||||||
 | 
					      <tr key={index}>
 | 
				
			||||||
 | 
					        <td className="Stats-percent">{percent}%</td>
 | 
				
			||||||
 | 
					        <td className="Stats-count">{count}</td>
 | 
				
			||||||
 | 
					        <td className="Stats-value">{format(value)}</td>
 | 
				
			||||||
 | 
					      </tr>
 | 
				
			||||||
 | 
					    );
 | 
				
			||||||
 | 
					  });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  if (ranking.other > 0) {
 | 
				
			||||||
 | 
					    const percent = displayPercentage((ranking.other / total) * 100);
 | 
				
			||||||
 | 
					    entries.push(
 | 
				
			||||||
 | 
					      <tr key="other">
 | 
				
			||||||
 | 
					        <td className="Stats-percent">{percent}%</td>
 | 
				
			||||||
 | 
					        <td className="Stats-count">{ranking.other}</td>
 | 
				
			||||||
 | 
					        <td className="Stats-value">Other</td>
 | 
				
			||||||
 | 
					      </tr>
 | 
				
			||||||
 | 
					    );
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  if (ranking.unknown > 0) {
 | 
				
			||||||
 | 
					    const percent = displayPercentage((ranking.unknown / total) * 100);
 | 
				
			||||||
 | 
					    entries.push(
 | 
				
			||||||
 | 
					      <tr key="unknown">
 | 
				
			||||||
 | 
					        <td className="Stats-percent">{percent}%</td>
 | 
				
			||||||
 | 
					        <td className="Stats-count">{ranking.unknown}</td>
 | 
				
			||||||
 | 
					        <td className="Stats-value Stats-unknown">Unknown</td>
 | 
				
			||||||
 | 
					      </tr>
 | 
				
			||||||
 | 
					    );
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  return (
 | 
				
			||||||
 | 
					    <div className="Stats-category" key={key}>
 | 
				
			||||||
 | 
					      <table>
 | 
				
			||||||
 | 
					        <thead>
 | 
				
			||||||
 | 
					          <tr>
 | 
				
			||||||
 | 
					            <th className="Stats-percent" />
 | 
				
			||||||
 | 
					            <th className="Stats-count" />
 | 
				
			||||||
 | 
					            <th className="Stats-value">{label}</th>
 | 
				
			||||||
 | 
					          </tr>
 | 
				
			||||||
 | 
					        </thead>
 | 
				
			||||||
 | 
					        <tbody>{entries}</tbody>
 | 
				
			||||||
 | 
					      </table>
 | 
				
			||||||
 | 
					    </div>
 | 
				
			||||||
 | 
					  );
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					function identity(value: string | number): string {
 | 
				
			||||||
 | 
					  return value + '';
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					function formatMemory(value: Range): string {
 | 
				
			||||||
 | 
					  const [min, max] = value;
 | 
				
			||||||
 | 
					  if (min === 0) {
 | 
				
			||||||
 | 
					    return 'Less than ' + max + ' GB';
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					  if (max === null) {
 | 
				
			||||||
 | 
					    return 'At least ' + min + ' GB';
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					  return min + ' GB';
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					function formatYesNo(value: boolean): string {
 | 
				
			||||||
 | 
					  if (value) {
 | 
				
			||||||
 | 
					    return 'Yes';
 | 
				
			||||||
 | 
					  } else {
 | 
				
			||||||
 | 
					    return 'No';
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					function formatScore(value: Range): string {
 | 
				
			||||||
 | 
					  const [min, max] = value;
 | 
				
			||||||
 | 
					  if (max === null) {
 | 
				
			||||||
 | 
					    return 'More than ' + (min / 100).toFixed(1) + 'x';
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					  if (min === 0) {
 | 
				
			||||||
 | 
					    return 'Less than ' + (max / 100).toFixed(1) + 'x';
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					  if (min <= 100 && max >= 100) {
 | 
				
			||||||
 | 
					    return 'Baseline';
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					  return (min / 100).toFixed(1) + 'x';
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export class Stats extends React.Component<StatsProps> {
 | 
				
			||||||
 | 
					  public componentDidMount() {
 | 
				
			||||||
 | 
					    ReactGA.send({ hitType: "pageview", page: "/stats" });
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  public render() {
 | 
				
			||||||
 | 
					    const { appState } = this.props;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    const children: React.ReactNode[] = [];
 | 
				
			||||||
 | 
					    function add<T>(
 | 
				
			||||||
 | 
					      key: string,
 | 
				
			||||||
 | 
					      label: string,
 | 
				
			||||||
 | 
					      format: (value: T) => string,
 | 
				
			||||||
 | 
					      ranking: Maybe<Ranking<T>>
 | 
				
			||||||
 | 
					    ) {
 | 
				
			||||||
 | 
					      if (ranking) {
 | 
				
			||||||
 | 
					        const child = generateRankingTable(key, label, format, ranking);
 | 
				
			||||||
 | 
					        if (child !== null) {
 | 
				
			||||||
 | 
					          children.push(child);
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    const stats = appState.chainStats;
 | 
				
			||||||
 | 
					    if (stats) {
 | 
				
			||||||
 | 
					      add('version', 'Version', identity, stats.version);
 | 
				
			||||||
 | 
					      add('target_os', 'Operating System', identity, stats.target_os);
 | 
				
			||||||
 | 
					      add('target_arch', 'CPU Architecture', identity, stats.target_arch);
 | 
				
			||||||
 | 
					      add('cpu', 'CPU', identity, stats.cpu);
 | 
				
			||||||
 | 
					      add('core_count', 'CPU Cores', identity, stats.core_count);
 | 
				
			||||||
 | 
					      add('cpu_vendor', 'CPU Vendor', identity, stats.cpu_vendor);
 | 
				
			||||||
 | 
					      add('memory', 'Memory', formatMemory, stats.memory);
 | 
				
			||||||
 | 
					      add(
 | 
				
			||||||
 | 
					        'is_virtual_machine',
 | 
				
			||||||
 | 
					        'Is Virtual Machine?',
 | 
				
			||||||
 | 
					        formatYesNo,
 | 
				
			||||||
 | 
					        stats.is_virtual_machine
 | 
				
			||||||
 | 
					      );
 | 
				
			||||||
 | 
					      add('linux_distro', 'Linux Distribution', identity, stats.linux_distro);
 | 
				
			||||||
 | 
					      add('linux_kernel', 'Linux Kernel', identity, stats.linux_kernel);
 | 
				
			||||||
 | 
					      add(
 | 
				
			||||||
 | 
					        'cpu_hashrate_score',
 | 
				
			||||||
 | 
					        'CPU Speed',
 | 
				
			||||||
 | 
					        formatScore,
 | 
				
			||||||
 | 
					        stats.cpu_hashrate_score
 | 
				
			||||||
 | 
					      );
 | 
				
			||||||
 | 
					      add(
 | 
				
			||||||
 | 
					        'memory_memcpy_score',
 | 
				
			||||||
 | 
					        'Memory Speed',
 | 
				
			||||||
 | 
					        formatScore,
 | 
				
			||||||
 | 
					        stats.memory_memcpy_score
 | 
				
			||||||
 | 
					      );
 | 
				
			||||||
 | 
					      add(
 | 
				
			||||||
 | 
					        'disk_sequential_write_score',
 | 
				
			||||||
 | 
					        'Disk Speed (sequential writes)',
 | 
				
			||||||
 | 
					        formatScore,
 | 
				
			||||||
 | 
					        stats.disk_sequential_write_score
 | 
				
			||||||
 | 
					      );
 | 
				
			||||||
 | 
					      add(
 | 
				
			||||||
 | 
					        'disk_random_write_score',
 | 
				
			||||||
 | 
					        'Disk Speed (random writes)',
 | 
				
			||||||
 | 
					        formatScore,
 | 
				
			||||||
 | 
					        stats.disk_random_write_score
 | 
				
			||||||
 | 
					      );
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    return <div className="Stats">{children}</div>;
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
							
								
								
									
										1
									
								
								src/components/Stats/index.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						@ -0,0 +1 @@
 | 
				
			|||||||
 | 
					export * from './Stats';
 | 
				
			||||||
							
								
								
									
										43
									
								
								src/components/Tile.css
									
									
									
									
									
										Normal file
									
								
							
							
						
						@ -0,0 +1,43 @@
 | 
				
			|||||||
 | 
					.Tile {
 | 
				
			||||||
 | 
					  font-size: 2.5em;
 | 
				
			||||||
 | 
					  text-align: left;
 | 
				
			||||||
 | 
					  width: 208px;
 | 
				
			||||||
 | 
					  height: 100px;
 | 
				
			||||||
 | 
					  display: inline-block;
 | 
				
			||||||
 | 
					  position: relative;
 | 
				
			||||||
 | 
					  color: #fff;
 | 
				
			||||||
 | 
					  cursor: pointer;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					.Tile:hover {
 | 
				
			||||||
 | 
					  color: #f2e370;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					.Tile-label {
 | 
				
			||||||
 | 
					  position: absolute;
 | 
				
			||||||
 | 
					  top: 24px;
 | 
				
			||||||
 | 
					  left: 50px;
 | 
				
			||||||
 | 
					  right: 0;
 | 
				
			||||||
 | 
					  font-size: 0.4em;
 | 
				
			||||||
 | 
					  text-transform: uppercase;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					.Tile-content {
 | 
				
			||||||
 | 
					  position: absolute;
 | 
				
			||||||
 | 
					  bottom: 16px;
 | 
				
			||||||
 | 
					  left: 50px;
 | 
				
			||||||
 | 
					  right: 0;
 | 
				
			||||||
 | 
					  font-weight: 300;
 | 
				
			||||||
 | 
					  font-size: 0.75em;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					.Tile .Icon {
 | 
				
			||||||
 | 
					  position: absolute;
 | 
				
			||||||
 | 
					  left: 20px;
 | 
				
			||||||
 | 
					  top: 15px;
 | 
				
			||||||
 | 
					  font-size: 0.8em;
 | 
				
			||||||
 | 
					  padding: 0.1em;
 | 
				
			||||||
 | 
					  border-radius: 1.25em;
 | 
				
			||||||
 | 
					  border: 2px solid #e6007a;
 | 
				
			||||||
 | 
					  color: #e6007a;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
							
								
								
									
										17
									
								
								src/components/Tile.tsx
									
									
									
									
									
										Normal file
									
								
							
							
						
						@ -0,0 +1,17 @@
 | 
				
			|||||||
 | 
					import * as React from 'react';
 | 
				
			||||||
 | 
					import './Tile.css';
 | 
				
			||||||
 | 
					import { Icon } from './Icon';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					interface TileProps {
 | 
				
			||||||
 | 
					  title: string;
 | 
				
			||||||
 | 
					  children?: React.ReactNode;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export function Tile(props: TileProps) {
 | 
				
			||||||
 | 
					  return (
 | 
				
			||||||
 | 
					    <div className="Tile">
 | 
				
			||||||
 | 
					      <span className="Tile-label">{props.title}</span>
 | 
				
			||||||
 | 
					      <span className="Tile-content">{props.children}</span>
 | 
				
			||||||
 | 
					    </div>
 | 
				
			||||||
 | 
					  );
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
							
								
								
									
										80
									
								
								src/components/Tooltip.css
									
									
									
									
									
										Normal file
									
								
							
							
						
						@ -0,0 +1,80 @@
 | 
				
			|||||||
 | 
					.Tooltip {
 | 
				
			||||||
 | 
					  background: #000;
 | 
				
			||||||
 | 
					  color: #fff;
 | 
				
			||||||
 | 
					  font-family: 'Ubuntu', sans-serif;
 | 
				
			||||||
 | 
					  font-size: 13px;
 | 
				
			||||||
 | 
					  font-weight: 400;
 | 
				
			||||||
 | 
					  padding: 3px 5px;
 | 
				
			||||||
 | 
					  border-radius: 2px;
 | 
				
			||||||
 | 
					  background: #1b3563;
 | 
				
			||||||
 | 
					  position: absolute;
 | 
				
			||||||
 | 
					  white-space: nowrap;
 | 
				
			||||||
 | 
					  top: -32px;
 | 
				
			||||||
 | 
					  left: 50%;
 | 
				
			||||||
 | 
					  transform: translateX(-50%);
 | 
				
			||||||
 | 
					  display: none;
 | 
				
			||||||
 | 
					  box-shadow: 0 3px 20px rgba(255, 255, 255, 0.2);
 | 
				
			||||||
 | 
					  pointer-events: none;
 | 
				
			||||||
 | 
					  transition: color 0.15s ease-in-out;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					.Tooltip::after {
 | 
				
			||||||
 | 
					  content: ' ';
 | 
				
			||||||
 | 
					  width: 0;
 | 
				
			||||||
 | 
					  height: 0;
 | 
				
			||||||
 | 
					  display: block;
 | 
				
			||||||
 | 
					  position: absolute;
 | 
				
			||||||
 | 
					  left: 50%;
 | 
				
			||||||
 | 
					  bottom: -6px;
 | 
				
			||||||
 | 
					  margin-left: -6px;
 | 
				
			||||||
 | 
					  border-top: 6px #1b3563 solid;
 | 
				
			||||||
 | 
					  border-left: 6px transparent solid;
 | 
				
			||||||
 | 
					  border-right: 6px transparent solid;
 | 
				
			||||||
 | 
					  pointer-events: none;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					.Tooltip-left {
 | 
				
			||||||
 | 
					  left: 10px;
 | 
				
			||||||
 | 
					  transform: none;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					.Tooltip-left::after {
 | 
				
			||||||
 | 
					  left: 3px;
 | 
				
			||||||
 | 
					  margin: 0;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					.Tooltip-right {
 | 
				
			||||||
 | 
					  left: initial;
 | 
				
			||||||
 | 
					  right: 10px;
 | 
				
			||||||
 | 
					  transform: none;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					.Tooltip-right::after {
 | 
				
			||||||
 | 
					  left: initial;
 | 
				
			||||||
 | 
					  right: 3px;
 | 
				
			||||||
 | 
					  margin: 0;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					.Tooltip.Tooltip-copied {
 | 
				
			||||||
 | 
					  color: #fff;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					.Tooltip-container {
 | 
				
			||||||
 | 
					  position: relative;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					.Tooltip-container-inline {
 | 
				
			||||||
 | 
					  display: inline-block;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					.Tooltip-container-inline .Tooltip-left {
 | 
				
			||||||
 | 
					  left: 0;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					.Tooltip-container-inline .Tooltip-right {
 | 
				
			||||||
 | 
					  right: 0;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					:hover > .Tooltip {
 | 
				
			||||||
 | 
					  display: block;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
							
								
								
									
										96
									
								
								src/components/Tooltip.tsx
									
									
									
									
									
										Normal file
									
								
							
							
						
						@ -0,0 +1,96 @@
 | 
				
			|||||||
 | 
					import * as React from 'react';
 | 
				
			||||||
 | 
					import { Maybe } from '../common';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					import './Tooltip.css';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					interface TooltipProps {
 | 
				
			||||||
 | 
					  text: string;
 | 
				
			||||||
 | 
					  copy?: (cb: TooltipCopyCallback) => void;
 | 
				
			||||||
 | 
					  position?: 'left' | 'right' | 'center';
 | 
				
			||||||
 | 
					  onInit?: (update: TooltipUpdateCallback) => void;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export type TooltipUpdateCallback = (text: string) => void;
 | 
				
			||||||
 | 
					export type TooltipCopyCallback = Maybe<() => void>;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					function copyToClipboard(text: string) {
 | 
				
			||||||
 | 
					  const el = document.createElement('textarea');
 | 
				
			||||||
 | 
					  el.value = text;
 | 
				
			||||||
 | 
					  document.body.appendChild(el);
 | 
				
			||||||
 | 
					  el.select();
 | 
				
			||||||
 | 
					  document.execCommand('copy');
 | 
				
			||||||
 | 
					  document.body.removeChild(el);
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export function Tooltip({
 | 
				
			||||||
 | 
					  text,
 | 
				
			||||||
 | 
					  position,
 | 
				
			||||||
 | 
					  copy,
 | 
				
			||||||
 | 
					  onInit,
 | 
				
			||||||
 | 
					}: TooltipProps): JSX.Element {
 | 
				
			||||||
 | 
					  const [copied, setCopied] = React.useState<boolean>(false);
 | 
				
			||||||
 | 
					  const [timer, setTimer] = React.useState<NodeJS.Timer | null>(null);
 | 
				
			||||||
 | 
					  const el = React.useRef<HTMLDivElement>(null);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  const update = React.useCallback(
 | 
				
			||||||
 | 
					    (newText: string) => {
 | 
				
			||||||
 | 
					      if (el.current) {
 | 
				
			||||||
 | 
					        el.current.textContent = newText;
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					    },
 | 
				
			||||||
 | 
					    [el]
 | 
				
			||||||
 | 
					  );
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  function restore() {
 | 
				
			||||||
 | 
					    setCopied(false);
 | 
				
			||||||
 | 
					    setTimer(null);
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  function onClick() {
 | 
				
			||||||
 | 
					    copyToClipboard(text);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    if (timer) {
 | 
				
			||||||
 | 
					      clearTimeout(timer);
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    setCopied(true);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    setTimer(setTimeout(restore, 2000));
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  React.useEffect(() => {
 | 
				
			||||||
 | 
					    if (onInit) {
 | 
				
			||||||
 | 
					      onInit(update);
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    if (copy) {
 | 
				
			||||||
 | 
					      copy(onClick);
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    return () => {
 | 
				
			||||||
 | 
					      if (timer) {
 | 
				
			||||||
 | 
					        clearTimeout(timer);
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      if (copy) {
 | 
				
			||||||
 | 
					        copy(null);
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					    };
 | 
				
			||||||
 | 
					  }, []);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  let tooltipClass = 'Tooltip';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  if (position && position !== 'center') {
 | 
				
			||||||
 | 
					    tooltipClass += ` Tooltip-${position}`;
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  if (copied) {
 | 
				
			||||||
 | 
					    tooltipClass += ' Tooltip-copied';
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  return (
 | 
				
			||||||
 | 
					    <div className={tooltipClass} ref={el}>
 | 
				
			||||||
 | 
					      {copied ? 'Copied to clipboard!' : text}
 | 
				
			||||||
 | 
					    </div>
 | 
				
			||||||
 | 
					  );
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
							
								
								
									
										30
									
								
								src/components/Truncate.tsx
									
									
									
									
									
										Normal file
									
								
							
							
						
						@ -0,0 +1,30 @@
 | 
				
			|||||||
 | 
					import * as React from 'react';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					interface TruncateProps {
 | 
				
			||||||
 | 
					  text: string;
 | 
				
			||||||
 | 
					  chars?: number;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export class Truncate extends React.Component<TruncateProps> {
 | 
				
			||||||
 | 
					  public shouldComponentUpdate(nextProps: TruncateProps): boolean {
 | 
				
			||||||
 | 
					    return this.props.text !== nextProps.text;
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  public render() {
 | 
				
			||||||
 | 
					    const { text, chars } = this.props;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    if (!text) {
 | 
				
			||||||
 | 
					      return '-';
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    if (chars != null && text.length <= chars) {
 | 
				
			||||||
 | 
					      return text;
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    return chars ? (
 | 
				
			||||||
 | 
					      `${text.substr(0, chars - 1)}…`
 | 
				
			||||||
 | 
					    ) : (
 | 
				
			||||||
 | 
					      <div className="Column-truncate">{text}</div>
 | 
				
			||||||
 | 
					    );
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
							
								
								
									
										11
									
								
								src/components/index.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						@ -0,0 +1,11 @@
 | 
				
			|||||||
 | 
					export * from './Chain';
 | 
				
			||||||
 | 
					export * from './List';
 | 
				
			||||||
 | 
					export * from './Map';
 | 
				
			||||||
 | 
					export * from './Stats';
 | 
				
			||||||
 | 
					export * from './Icon';
 | 
				
			||||||
 | 
					export * from './Tile';
 | 
				
			||||||
 | 
					export * from './Ago';
 | 
				
			||||||
 | 
					export * from './Sparkline';
 | 
				
			||||||
 | 
					export * from './Truncate';
 | 
				
			||||||
 | 
					export * from './Tooltip';
 | 
				
			||||||
 | 
					export * from './Filter';
 | 
				
			||||||
							
								
								
									
										1
									
								
								src/icons/alert.svg
									
									
									
									
									
										Normal file
									
								
							
							
						
						@ -0,0 +1 @@
 | 
				
			|||||||
 | 
					<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 16 16"><path fill-rule="evenodd" d="M8.893 1.5c-.183-.31-.52-.5-.887-.5s-.703.19-.886.5L.138 13.499a.98.98 0 0 0 0 1.001c.193.31.53.501.886.501h13.964c.367 0 .704-.19.877-.5a1.03 1.03 0 0 0 .01-1.002L8.893 1.5zm.133 11.497H6.987v-2.003h2.039v2.003zm0-3.004H6.987V5.987h2.039v4.006z"/></svg>
 | 
				
			||||||
| 
		 After Width: | Height: | Size: 366 B  | 
							
								
								
									
										1
									
								
								src/icons/archive.svg
									
									
									
									
									
										Normal file
									
								
							
							
						
						@ -0,0 +1 @@
 | 
				
			|||||||
 | 
					<svg xmlns="http://www.w3.org/2000/svg" width="14" height="16" viewBox="0 0 14 16"><path fill-rule="evenodd" d="M13 2H1v2h12V2zM0 4a1 1 0 0 0 1 1v9a1 1 0 0 0 1 1h10a1 1 0 0 0 1-1V5a1 1 0 0 0 1-1V2a1 1 0 0 0-1-1H1a1 1 0 0 0-1 1v2zm2 1h10v9H2V5zm2 3h6V7H4v1z"/></svg>
 | 
				
			||||||
| 
		 After Width: | Height: | Size: 265 B  |