├── .env.template ├── .eslintignore ├── .eslintrc.js ├── .github └── workflows │ ├── actions.yml │ └── cloudflare-deploy.yml ├── .gitignore ├── .prettierrc.js ├── .vscode └── settings.json ├── LICENSE ├── README.md ├── copyright.txt ├── craco.config.js ├── graphql-schema.json ├── license-check-and-add-config.json ├── package.json ├── public ├── icon.png ├── index.html ├── manifest.json └── robots.txt ├── src ├── App.tsx ├── Components │ ├── Elements │ │ ├── Icons │ │ │ └── ToolTipIcon.tsx │ │ ├── Pagination.tsx │ │ └── StatTitles.tsx │ ├── Layouts │ │ ├── BodyLayout.tsx │ │ ├── GridLayout │ │ │ └── GridLayoutWrapper.tsx │ │ └── SectionTile │ │ │ ├── CardStat.tsx │ │ │ ├── SectionBody.tsx │ │ │ ├── SectionCard.tsx │ │ │ └── SectionTile.tsx │ ├── Modules │ │ ├── CountryStats │ │ │ ├── CountryBox.tsx │ │ │ ├── StatsChartBox.tsx │ │ │ └── index.tsx │ │ ├── DemographicsStats │ │ │ ├── ClientTypes.tsx │ │ │ ├── NodeCount12.tsx │ │ │ ├── NodeReadyForFork.tsx │ │ │ └── StatusSync.tsx │ │ ├── Footer.tsx │ │ ├── HeatMap │ │ │ └── MapLeaflet.tsx │ │ ├── Navbar.tsx │ │ ├── NodeStats │ │ │ └── NodeStatsOverTime.tsx │ │ └── SoftwareStats │ │ │ ├── AltAirPercentage.tsx │ │ │ ├── NetworkTypes.tsx │ │ │ ├── OperatingSystems.tsx │ │ │ ├── PercentageOfNodes.tsx │ │ │ └── VersionVariance.tsx │ ├── Pages │ │ └── HomePage.tsx │ └── Themes │ │ ├── constants.ts │ │ ├── theme.ts │ │ └── types.ts ├── Contexts │ └── Eth2CrawlerContext.tsx ├── GraphQL │ ├── Queries.ts │ └── types │ │ ├── GetAltAirUpgradePercentage.ts │ │ ├── GetClientCounts.ts │ │ ├── GetClientVersions.ts │ │ ├── GetHeatmap.ts │ │ ├── GetNetworks.ts │ │ ├── GetNodeStats.ts │ │ ├── GetNodeStatsOverTime.ts │ │ ├── GetNodesByCountries.ts │ │ ├── GetOperatingSystems.ts │ │ └── getRegionalStats.ts ├── assets │ └── fonts │ │ └── Neue-montreal │ │ ├── NeueMontreal-Bold.otf │ │ ├── NeueMontreal-BoldItalic.otf │ │ ├── NeueMontreal-Italic.otf │ │ ├── NeueMontreal-Light.otf │ │ ├── NeueMontreal-LightItalic.otf │ │ ├── NeueMontreal-Medium.otf │ │ ├── NeueMontreal-MediumItalic.otf │ │ └── NeueMontreal-Regular.otf ├── dummyData │ ├── demographicsData.ts │ └── mapData.ts ├── index.css ├── index.tsx ├── react-app-env.d.ts ├── reportWebVitals.ts ├── setupTests.ts ├── types │ ├── graphql-global-types.ts │ ├── index.d.ts │ └── main.ts ├── utilHooks │ └── useWindowDimensions.ts └── utils │ └── dateUtils.ts ├── tsconfig.json └── yarn.lock /.env.template: -------------------------------------------------------------------------------- 1 | REACT_APP_GRAPHQL_URL=http://localhost:6969/graphql 2 | -------------------------------------------------------------------------------- /.eslintignore: -------------------------------------------------------------------------------- 1 | # Copyright 2021 ChainSafe Systems 2 | # SPDX-License-Identifier: LGPL-3.0-only 3 | .eslintrc.js -------------------------------------------------------------------------------- /.eslintrc.js: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2021 ChainSafe Systems 3 | SPDX-License-Identifier: LGPL-3.0-only 4 | */ 5 | module.exports = { 6 | env: { 7 | browser: true, 8 | es6: true, 9 | }, 10 | extends: [ 11 | "eslint:recommended", 12 | "plugin:react/recommended", 13 | "plugin:@typescript-eslint/eslint-recommended", 14 | "prettier", 15 | "plugin:react-hooks/recommended", 16 | ], 17 | globals: { 18 | Atomics: "readonly", 19 | SharedArrayBuffer: "readonly", 20 | }, 21 | parser: "@typescript-eslint/parser", 22 | parserOptions: { 23 | ecmaFeatures: { 24 | jsx: true, 25 | }, 26 | ecmaVersion: 2018, 27 | sourceType: "module", 28 | }, 29 | settings: { 30 | react: { 31 | version: "detect", 32 | }, 33 | }, 34 | plugins: ["react", "@typescript-eslint", "prettier"], 35 | rules: { 36 | "linebreak-style": ["error", "unix"], 37 | "react/prop-types": 0, 38 | "no-unused-vars": "warn", 39 | "no-console": ["warn", { allow: ["warn", "error"] }], 40 | }, 41 | } 42 | -------------------------------------------------------------------------------- /.github/workflows/actions.yml: -------------------------------------------------------------------------------- 1 | name: React.TS install, lint, build, License 2 | 3 | on: [pull_request] 4 | 5 | jobs: 6 | build: 7 | runs-on: ubuntu-18.04 8 | 9 | strategy: 10 | matrix: 11 | node-version: [14.4.0] 12 | 13 | steps: 14 | - uses: actions/checkout@v2 15 | - name: Use Node.js ${{ matrix.node-version }} 16 | uses: actions/setup-node@v1 17 | with: 18 | node-version: ${{ matrix.node-version }} 19 | - name: yarn install, lint, build 20 | run: | 21 | yarn install --frozen-lockfile 22 | yarn run lint 23 | yarn run license-check 24 | yarn run build --if-present 25 | # yarn test 26 | env: 27 | CI: true 28 | -------------------------------------------------------------------------------- /.github/workflows/cloudflare-deploy.yml: -------------------------------------------------------------------------------- 1 | name: CloudFlare Deploy 2 | on: [push] 3 | 4 | jobs: 5 | deploy: 6 | runs-on: ubuntu-18.04 7 | permissions: 8 | contents: read 9 | deployments: write 10 | steps: 11 | - uses: actions/checkout@v3 12 | - uses: actions/setup-node@v3 13 | with: 14 | cache: yarn 15 | node-version: '14.4.0' 16 | - run: yarn install --frozen-lockfile 17 | - run: yarn run build 18 | - uses: actions/setup-node@v3 19 | with: 20 | node-version: '16' 21 | - name: Publish to Cloudflare Pages 22 | uses: cloudflare/pages-action@1 23 | with: 24 | apiToken: ${{ secrets.CLOUDFLARE_API_TOKEN }} 25 | accountId: ${{ secrets.CLOUDFLARE_ACCOUNT_ID }} 26 | projectName: nodewatch 27 | directory: ./build 28 | gitHubToken: ${{ secrets.GITHUB_TOKEN }} 29 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files. 2 | 3 | # dependencies 4 | /node_modules 5 | /.pnp 6 | .pnp.js 7 | 8 | # testing 9 | /coverage 10 | 11 | # production 12 | /build 13 | 14 | # misc 15 | .DS_Store 16 | .env 17 | .env.local 18 | .env.development.local 19 | .env.test.local 20 | .env.production.local 21 | 22 | npm-debug.log* 23 | yarn-debug.log* 24 | yarn-error.log* 25 | 26 | .vscode 27 | .env 28 | .idea/ 29 | -------------------------------------------------------------------------------- /.prettierrc.js: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2021 ChainSafe Systems 3 | SPDX-License-Identifier: LGPL-3.0-only 4 | */ 5 | module.exports = { 6 | trailingComma: "es5", 7 | semi: false, 8 | singleQuote: false, 9 | printWidth: 100, 10 | } 11 | -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "cSpell.words": [ 3 | "chartjs", 4 | "clsx", 5 | "craco", 6 | "getc", 7 | "HEATMAP", 8 | "multigeth", 9 | "nethermind", 10 | "nonhosted", 11 | "unsynced" 12 | ] 13 | } 14 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | GNU LESSER GENERAL PUBLIC LICENSE 2 | Version 3, 29 June 2007 3 | 4 | Copyright (C) 2007 Free Software Foundation, Inc. 5 | Everyone is permitted to copy and distribute verbatim copies 6 | of this license document, but changing it is not allowed. 7 | 8 | 9 | This version of the GNU Lesser General Public License incorporates 10 | the terms and conditions of version 3 of the GNU General Public 11 | License, supplemented by the additional permissions listed below. 12 | 13 | 0. Additional Definitions. 14 | 15 | As used herein, "this License" refers to version 3 of the GNU Lesser 16 | General Public License, and the "GNU GPL" refers to version 3 of the GNU 17 | General Public License. 18 | 19 | "The Library" refers to a covered work governed by this License, 20 | other than an Application or a Combined Work as defined below. 21 | 22 | An "Application" is any work that makes use of an interface provided 23 | by the Library, but which is not otherwise based on the Library. 24 | Defining a subclass of a class defined by the Library is deemed a mode 25 | of using an interface provided by the Library. 26 | 27 | A "Combined Work" is a work produced by combining or linking an 28 | Application with the Library. The particular version of the Library 29 | with which the Combined Work was made is also called the "Linked 30 | Version". 31 | 32 | The "Minimal Corresponding Source" for a Combined Work means the 33 | Corresponding Source for the Combined Work, excluding any source code 34 | for portions of the Combined Work that, considered in isolation, are 35 | based on the Application, and not on the Linked Version. 36 | 37 | The "Corresponding Application Code" for a Combined Work means the 38 | object code and/or source code for the Application, including any data 39 | and utility programs needed for reproducing the Combined Work from the 40 | Application, but excluding the System Libraries of the Combined Work. 41 | 42 | 1. Exception to Section 3 of the GNU GPL. 43 | 44 | You may convey a covered work under sections 3 and 4 of this License 45 | without being bound by section 3 of the GNU GPL. 46 | 47 | 2. Conveying Modified Versions. 48 | 49 | If you modify a copy of the Library, and, in your modifications, a 50 | facility refers to a function or data to be supplied by an Application 51 | that uses the facility (other than as an argument passed when the 52 | facility is invoked), then you may convey a copy of the modified 53 | version: 54 | 55 | a) under this License, provided that you make a good faith effort to 56 | ensure that, in the event an Application does not supply the 57 | function or data, the facility still operates, and performs 58 | whatever part of its purpose remains meaningful, or 59 | 60 | b) under the GNU GPL, with none of the additional permissions of 61 | this License applicable to that copy. 62 | 63 | 3. Object Code Incorporating Material from Library Header Files. 64 | 65 | The object code form of an Application may incorporate material from 66 | a header file that is part of the Library. You may convey such object 67 | code under terms of your choice, provided that, if the incorporated 68 | material is not limited to numerical parameters, data structure 69 | layouts and accessors, or small macros, inline functions and templates 70 | (ten or fewer lines in length), you do both of the following: 71 | 72 | a) Give prominent notice with each copy of the object code that the 73 | Library is used in it and that the Library and its use are 74 | covered by this License. 75 | 76 | b) Accompany the object code with a copy of the GNU GPL and this license 77 | document. 78 | 79 | 4. Combined Works. 80 | 81 | You may convey a Combined Work under terms of your choice that, 82 | taken together, effectively do not restrict modification of the 83 | portions of the Library contained in the Combined Work and reverse 84 | engineering for debugging such modifications, if you also do each of 85 | the following: 86 | 87 | a) Give prominent notice with each copy of the Combined Work that 88 | the Library is used in it and that the Library and its use are 89 | covered by this License. 90 | 91 | b) Accompany the Combined Work with a copy of the GNU GPL and this license 92 | document. 93 | 94 | c) For a Combined Work that displays copyright notices during 95 | execution, include the copyright notice for the Library among 96 | these notices, as well as a reference directing the user to the 97 | copies of the GNU GPL and this license document. 98 | 99 | d) Do one of the following: 100 | 101 | 0) Convey the Minimal Corresponding Source under the terms of this 102 | License, and the Corresponding Application Code in a form 103 | suitable for, and under terms that permit, the user to 104 | recombine or relink the Application with a modified version of 105 | the Linked Version to produce a modified Combined Work, in the 106 | manner specified by section 6 of the GNU GPL for conveying 107 | Corresponding Source. 108 | 109 | 1) Use a suitable shared library mechanism for linking with the 110 | Library. A suitable mechanism is one that (a) uses at run time 111 | a copy of the Library already present on the user's computer 112 | system, and (b) will operate properly with a modified version 113 | of the Library that is interface-compatible with the Linked 114 | Version. 115 | 116 | e) Provide Installation Information, but only if you would otherwise 117 | be required to provide such information under section 6 of the 118 | GNU GPL, and only to the extent that such information is 119 | necessary to install and execute a modified version of the 120 | Combined Work produced by recombining or relinking the 121 | Application with a modified version of the Linked Version. (If 122 | you use option 4d0, the Installation Information must accompany 123 | the Minimal Corresponding Source and Corresponding Application 124 | Code. If you use option 4d1, you must provide the Installation 125 | Information in the manner specified by section 6 of the GNU GPL 126 | for conveying Corresponding Source.) 127 | 128 | 5. Combined Libraries. 129 | 130 | You may place library facilities that are a work based on the 131 | Library side by side in a single library together with other library 132 | facilities that are not Applications and are not covered by this 133 | License, and convey such a combined library under terms of your 134 | choice, if you do both of the following: 135 | 136 | a) Accompany the combined library with a copy of the same work based 137 | on the Library, uncombined with any other library facilities, 138 | conveyed under the terms of this License. 139 | 140 | b) Give prominent notice with the combined library that part of it 141 | is a work based on the Library, and explaining where to find the 142 | accompanying uncombined form of the same work. 143 | 144 | 6. Revised Versions of the GNU Lesser General Public License. 145 | 146 | The Free Software Foundation may publish revised and/or new versions 147 | of the GNU Lesser General Public License from time to time. Such new 148 | versions will be similar in spirit to the present version, but may 149 | differ in detail to address new problems or concerns. 150 | 151 | Each version is given a distinguishing version number. If the 152 | Library as you received it specifies that a certain numbered version 153 | of the GNU Lesser General Public License "or any later version" 154 | applies to it, you have the option of following the terms and 155 | conditions either of that published version or of any later version 156 | published by the Free Software Foundation. If the Library as you 157 | received it does not specify a version number of the GNU Lesser 158 | General Public License, you may choose any version of the GNU Lesser 159 | General Public License ever published by the Free Software Foundation. 160 | 161 | If the Library as you received it specifies that a proxy can decide 162 | whether future versions of the GNU Lesser General Public License shall 163 | apply, that proxy's public statement of acceptance of any version is 164 | permanent authorization for you to choose that version for the 165 | Library. -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # NodeWatch Frontend (UI) 2 | 3 | A web-based user interface will be developed to display the data collected by a [devp2p crawler](https://github.com/ChainSafe/eth2-crawler) targeted at Eth2 nodes. It will contain the following: 4 | 5 | - Client breakdown by agent type 6 | - Toggle connectable vs all seen in last month 7 | - Client-version breakdown 8 | - Regional breakdown with map 9 | - IP type where possible – hosted, residential, etc 10 | 11 | # Running the project 12 | 13 | This project was bootstrapped with [Create React App](https://github.com/facebook/create-react-app) and using [Craco](https://github.com/gsoft-inc/craco) for configuration. 14 | 15 | Provide the crawler graphql endpoint in a `.env` file in root as per `.env.template`. 16 | 17 | To run in development mode 18 | 19 | ``` 20 | yarn install 21 | yarn start 22 | ``` 23 | 24 | To build 25 | 26 | ``` 27 | yarn install 28 | yarn build 29 | ``` 30 | 31 | # LICENSE 32 | 33 | See the [LICENSE](https://github.com/ChainSafe/eth2-crawler-ui/blob/main/LICENSE) file for license rights and limitations (lgpl-3.0). 34 | -------------------------------------------------------------------------------- /copyright.txt: -------------------------------------------------------------------------------- 1 | Copyright 2021 ChainSafe Systems 2 | SPDX-License-Identifier: LGPL-3.0-only 3 | -------------------------------------------------------------------------------- /craco.config.js: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2021 ChainSafe Systems 3 | SPDX-License-Identifier: LGPL-3.0-only 4 | */ 5 | /* eslint-disable no-undef */ 6 | const path = require("path") 7 | const fs = require("fs") 8 | 9 | const cracoBabelLoader = require("craco-babel-loader") 10 | 11 | const appDirectory = fs.realpathSync(process.cwd()) 12 | const resolveApp = (relativePath) => path.resolve(appDirectory, relativePath) 13 | 14 | // react-leaflet requires custom inclusion through babel 15 | module.exports = { 16 | plugins: [ 17 | { 18 | plugin: cracoBabelLoader, 19 | options: { 20 | includes: [resolveApp("node_modules/@react-leaflet"), resolveApp("node_modules/react-leaflet")], 21 | }, 22 | }, 23 | ], 24 | } 25 | -------------------------------------------------------------------------------- /graphql-schema.json: -------------------------------------------------------------------------------- 1 | { 2 | "__schema": { 3 | "description": null, 4 | "queryType": { 5 | "name": "Query" 6 | }, 7 | "mutationType": null, 8 | "subscriptionType": null, 9 | "types": [ 10 | { 11 | "kind": "OBJECT", 12 | "name": "AggregateData", 13 | "description": "", 14 | "specifiedByUrl": null, 15 | "fields": [ 16 | { 17 | "name": "name", 18 | "description": "", 19 | "args": [], 20 | "type": { 21 | "kind": "NON_NULL", 22 | "name": null, 23 | "ofType": { 24 | "kind": "SCALAR", 25 | "name": "String", 26 | "ofType": null 27 | } 28 | }, 29 | "isDeprecated": false, 30 | "deprecationReason": null 31 | }, 32 | { 33 | "name": "count", 34 | "description": "", 35 | "args": [], 36 | "type": { 37 | "kind": "NON_NULL", 38 | "name": null, 39 | "ofType": { 40 | "kind": "SCALAR", 41 | "name": "Int", 42 | "ofType": null 43 | } 44 | }, 45 | "isDeprecated": false, 46 | "deprecationReason": null 47 | } 48 | ], 49 | "inputFields": null, 50 | "interfaces": [], 51 | "enumValues": null, 52 | "possibleTypes": null 53 | }, 54 | { 55 | "kind": "SCALAR", 56 | "name": "Boolean", 57 | "description": "The `Boolean` scalar type represents `true` or `false`.", 58 | "specifiedByUrl": null, 59 | "fields": null, 60 | "inputFields": null, 61 | "interfaces": null, 62 | "enumValues": null, 63 | "possibleTypes": null 64 | }, 65 | { 66 | "kind": "OBJECT", 67 | "name": "ClientVersionAggregation", 68 | "description": "", 69 | "specifiedByUrl": null, 70 | "fields": [ 71 | { 72 | "name": "client", 73 | "description": "", 74 | "args": [], 75 | "type": { 76 | "kind": "NON_NULL", 77 | "name": null, 78 | "ofType": { 79 | "kind": "SCALAR", 80 | "name": "String", 81 | "ofType": null 82 | } 83 | }, 84 | "isDeprecated": false, 85 | "deprecationReason": null 86 | }, 87 | { 88 | "name": "count", 89 | "description": "", 90 | "args": [], 91 | "type": { 92 | "kind": "NON_NULL", 93 | "name": null, 94 | "ofType": { 95 | "kind": "SCALAR", 96 | "name": "Int", 97 | "ofType": null 98 | } 99 | }, 100 | "isDeprecated": false, 101 | "deprecationReason": null 102 | }, 103 | { 104 | "name": "versions", 105 | "description": "", 106 | "args": [], 107 | "type": { 108 | "kind": "NON_NULL", 109 | "name": null, 110 | "ofType": { 111 | "kind": "LIST", 112 | "name": null, 113 | "ofType": { 114 | "kind": "NON_NULL", 115 | "name": null, 116 | "ofType": { 117 | "kind": "OBJECT", 118 | "name": "AggregateData", 119 | "ofType": null 120 | } 121 | } 122 | } 123 | }, 124 | "isDeprecated": false, 125 | "deprecationReason": null 126 | } 127 | ], 128 | "inputFields": null, 129 | "interfaces": [], 130 | "enumValues": null, 131 | "possibleTypes": null 132 | }, 133 | { 134 | "kind": "SCALAR", 135 | "name": "Float", 136 | "description": "The `Float` scalar type represents signed double-precision fractional values as specified by [IEEE 754](https://en.wikipedia.org/wiki/IEEE_floating_point).", 137 | "specifiedByUrl": null, 138 | "fields": null, 139 | "inputFields": null, 140 | "interfaces": null, 141 | "enumValues": null, 142 | "possibleTypes": null 143 | }, 144 | { 145 | "kind": "OBJECT", 146 | "name": "HeatmapData", 147 | "description": "", 148 | "specifiedByUrl": null, 149 | "fields": [ 150 | { 151 | "name": "networkType", 152 | "description": "", 153 | "args": [], 154 | "type": { 155 | "kind": "NON_NULL", 156 | "name": null, 157 | "ofType": { 158 | "kind": "SCALAR", 159 | "name": "String", 160 | "ofType": null 161 | } 162 | }, 163 | "isDeprecated": false, 164 | "deprecationReason": null 165 | }, 166 | { 167 | "name": "clientType", 168 | "description": "", 169 | "args": [], 170 | "type": { 171 | "kind": "NON_NULL", 172 | "name": null, 173 | "ofType": { 174 | "kind": "SCALAR", 175 | "name": "String", 176 | "ofType": null 177 | } 178 | }, 179 | "isDeprecated": false, 180 | "deprecationReason": null 181 | }, 182 | { 183 | "name": "syncStatus", 184 | "description": "", 185 | "args": [], 186 | "type": { 187 | "kind": "NON_NULL", 188 | "name": null, 189 | "ofType": { 190 | "kind": "SCALAR", 191 | "name": "String", 192 | "ofType": null 193 | } 194 | }, 195 | "isDeprecated": false, 196 | "deprecationReason": null 197 | }, 198 | { 199 | "name": "latitude", 200 | "description": "", 201 | "args": [], 202 | "type": { 203 | "kind": "NON_NULL", 204 | "name": null, 205 | "ofType": { 206 | "kind": "SCALAR", 207 | "name": "Float", 208 | "ofType": null 209 | } 210 | }, 211 | "isDeprecated": false, 212 | "deprecationReason": null 213 | }, 214 | { 215 | "name": "longitude", 216 | "description": "", 217 | "args": [], 218 | "type": { 219 | "kind": "NON_NULL", 220 | "name": null, 221 | "ofType": { 222 | "kind": "SCALAR", 223 | "name": "Float", 224 | "ofType": null 225 | } 226 | }, 227 | "isDeprecated": false, 228 | "deprecationReason": null 229 | }, 230 | { 231 | "name": "city", 232 | "description": "", 233 | "args": [], 234 | "type": { 235 | "kind": "NON_NULL", 236 | "name": null, 237 | "ofType": { 238 | "kind": "SCALAR", 239 | "name": "String", 240 | "ofType": null 241 | } 242 | }, 243 | "isDeprecated": false, 244 | "deprecationReason": null 245 | }, 246 | { 247 | "name": "country", 248 | "description": "", 249 | "args": [], 250 | "type": { 251 | "kind": "NON_NULL", 252 | "name": null, 253 | "ofType": { 254 | "kind": "SCALAR", 255 | "name": "String", 256 | "ofType": null 257 | } 258 | }, 259 | "isDeprecated": false, 260 | "deprecationReason": null 261 | } 262 | ], 263 | "inputFields": null, 264 | "interfaces": [], 265 | "enumValues": null, 266 | "possibleTypes": null 267 | }, 268 | { 269 | "kind": "SCALAR", 270 | "name": "ID", 271 | "description": "The `ID` scalar type represents a unique identifier, often used to refetch an object or as key for a cache. The ID type appears in a JSON response as a String; however, it is not intended to be human-readable. When expected as an input type, any string (such as `\"4\"`) or integer (such as `4`) input value will be accepted as an ID.", 272 | "specifiedByUrl": null, 273 | "fields": null, 274 | "inputFields": null, 275 | "interfaces": null, 276 | "enumValues": null, 277 | "possibleTypes": null 278 | }, 279 | { 280 | "kind": "SCALAR", 281 | "name": "Int", 282 | "description": "The `Int` scalar type represents non-fractional signed whole numeric values. Int can represent values between -(2^31) and 2^31 - 1.", 283 | "specifiedByUrl": null, 284 | "fields": null, 285 | "inputFields": null, 286 | "interfaces": null, 287 | "enumValues": null, 288 | "possibleTypes": null 289 | }, 290 | { 291 | "kind": "OBJECT", 292 | "name": "NodeStats", 293 | "description": "", 294 | "specifiedByUrl": null, 295 | "fields": [ 296 | { 297 | "name": "totalNodes", 298 | "description": "", 299 | "args": [], 300 | "type": { 301 | "kind": "NON_NULL", 302 | "name": null, 303 | "ofType": { 304 | "kind": "SCALAR", 305 | "name": "Int", 306 | "ofType": null 307 | } 308 | }, 309 | "isDeprecated": false, 310 | "deprecationReason": null 311 | }, 312 | { 313 | "name": "nodeSyncedPercentage", 314 | "description": "", 315 | "args": [], 316 | "type": { 317 | "kind": "NON_NULL", 318 | "name": null, 319 | "ofType": { 320 | "kind": "SCALAR", 321 | "name": "Float", 322 | "ofType": null 323 | } 324 | }, 325 | "isDeprecated": false, 326 | "deprecationReason": null 327 | }, 328 | { 329 | "name": "nodeUnsyncedPercentage", 330 | "description": "", 331 | "args": [], 332 | "type": { 333 | "kind": "NON_NULL", 334 | "name": null, 335 | "ofType": { 336 | "kind": "SCALAR", 337 | "name": "Float", 338 | "ofType": null 339 | } 340 | }, 341 | "isDeprecated": false, 342 | "deprecationReason": null 343 | } 344 | ], 345 | "inputFields": null, 346 | "interfaces": [], 347 | "enumValues": null, 348 | "possibleTypes": null 349 | }, 350 | { 351 | "kind": "OBJECT", 352 | "name": "NodeStatsOverTime", 353 | "description": "", 354 | "specifiedByUrl": null, 355 | "fields": [ 356 | { 357 | "name": "time", 358 | "description": "", 359 | "args": [], 360 | "type": { 361 | "kind": "NON_NULL", 362 | "name": null, 363 | "ofType": { 364 | "kind": "SCALAR", 365 | "name": "Float", 366 | "ofType": null 367 | } 368 | }, 369 | "isDeprecated": false, 370 | "deprecationReason": null 371 | }, 372 | { 373 | "name": "totalNodes", 374 | "description": "", 375 | "args": [], 376 | "type": { 377 | "kind": "NON_NULL", 378 | "name": null, 379 | "ofType": { 380 | "kind": "SCALAR", 381 | "name": "Int", 382 | "ofType": null 383 | } 384 | }, 385 | "isDeprecated": false, 386 | "deprecationReason": null 387 | }, 388 | { 389 | "name": "syncedNodes", 390 | "description": "", 391 | "args": [], 392 | "type": { 393 | "kind": "NON_NULL", 394 | "name": null, 395 | "ofType": { 396 | "kind": "SCALAR", 397 | "name": "Int", 398 | "ofType": null 399 | } 400 | }, 401 | "isDeprecated": false, 402 | "deprecationReason": null 403 | }, 404 | { 405 | "name": "unsyncedNodes", 406 | "description": "", 407 | "args": [], 408 | "type": { 409 | "kind": "NON_NULL", 410 | "name": null, 411 | "ofType": { 412 | "kind": "SCALAR", 413 | "name": "Int", 414 | "ofType": null 415 | } 416 | }, 417 | "isDeprecated": false, 418 | "deprecationReason": null 419 | } 420 | ], 421 | "inputFields": null, 422 | "interfaces": [], 423 | "enumValues": null, 424 | "possibleTypes": null 425 | }, 426 | { 427 | "kind": "OBJECT", 428 | "name": "Query", 429 | "description": "", 430 | "specifiedByUrl": null, 431 | "fields": [ 432 | { 433 | "name": "aggregateByAgentName", 434 | "description": "", 435 | "args": [], 436 | "type": { 437 | "kind": "NON_NULL", 438 | "name": null, 439 | "ofType": { 440 | "kind": "LIST", 441 | "name": null, 442 | "ofType": { 443 | "kind": "NON_NULL", 444 | "name": null, 445 | "ofType": { 446 | "kind": "OBJECT", 447 | "name": "AggregateData", 448 | "ofType": null 449 | } 450 | } 451 | } 452 | }, 453 | "isDeprecated": false, 454 | "deprecationReason": null 455 | }, 456 | { 457 | "name": "aggregateByCountry", 458 | "description": "", 459 | "args": [], 460 | "type": { 461 | "kind": "NON_NULL", 462 | "name": null, 463 | "ofType": { 464 | "kind": "LIST", 465 | "name": null, 466 | "ofType": { 467 | "kind": "NON_NULL", 468 | "name": null, 469 | "ofType": { 470 | "kind": "OBJECT", 471 | "name": "AggregateData", 472 | "ofType": null 473 | } 474 | } 475 | } 476 | }, 477 | "isDeprecated": false, 478 | "deprecationReason": null 479 | }, 480 | { 481 | "name": "aggregateByOperatingSystem", 482 | "description": "", 483 | "args": [], 484 | "type": { 485 | "kind": "NON_NULL", 486 | "name": null, 487 | "ofType": { 488 | "kind": "LIST", 489 | "name": null, 490 | "ofType": { 491 | "kind": "NON_NULL", 492 | "name": null, 493 | "ofType": { 494 | "kind": "OBJECT", 495 | "name": "AggregateData", 496 | "ofType": null 497 | } 498 | } 499 | } 500 | }, 501 | "isDeprecated": false, 502 | "deprecationReason": null 503 | }, 504 | { 505 | "name": "aggregateByNetwork", 506 | "description": "", 507 | "args": [], 508 | "type": { 509 | "kind": "NON_NULL", 510 | "name": null, 511 | "ofType": { 512 | "kind": "LIST", 513 | "name": null, 514 | "ofType": { 515 | "kind": "NON_NULL", 516 | "name": null, 517 | "ofType": { 518 | "kind": "OBJECT", 519 | "name": "AggregateData", 520 | "ofType": null 521 | } 522 | } 523 | } 524 | }, 525 | "isDeprecated": false, 526 | "deprecationReason": null 527 | }, 528 | { 529 | "name": "aggregateByClientVersion", 530 | "description": "", 531 | "args": [], 532 | "type": { 533 | "kind": "NON_NULL", 534 | "name": null, 535 | "ofType": { 536 | "kind": "LIST", 537 | "name": null, 538 | "ofType": { 539 | "kind": "NON_NULL", 540 | "name": null, 541 | "ofType": { 542 | "kind": "OBJECT", 543 | "name": "ClientVersionAggregation", 544 | "ofType": null 545 | } 546 | } 547 | } 548 | }, 549 | "isDeprecated": false, 550 | "deprecationReason": null 551 | }, 552 | { 553 | "name": "getHeatmapData", 554 | "description": "", 555 | "args": [], 556 | "type": { 557 | "kind": "NON_NULL", 558 | "name": null, 559 | "ofType": { 560 | "kind": "LIST", 561 | "name": null, 562 | "ofType": { 563 | "kind": "NON_NULL", 564 | "name": null, 565 | "ofType": { 566 | "kind": "OBJECT", 567 | "name": "HeatmapData", 568 | "ofType": null 569 | } 570 | } 571 | } 572 | }, 573 | "isDeprecated": false, 574 | "deprecationReason": null 575 | }, 576 | { 577 | "name": "getNodeStats", 578 | "description": "", 579 | "args": [], 580 | "type": { 581 | "kind": "NON_NULL", 582 | "name": null, 583 | "ofType": { 584 | "kind": "OBJECT", 585 | "name": "NodeStats", 586 | "ofType": null 587 | } 588 | }, 589 | "isDeprecated": false, 590 | "deprecationReason": null 591 | }, 592 | { 593 | "name": "getNodeStatsOverTime", 594 | "description": "", 595 | "args": [ 596 | { 597 | "name": "start", 598 | "description": "", 599 | "type": { 600 | "kind": "NON_NULL", 601 | "name": null, 602 | "ofType": { 603 | "kind": "SCALAR", 604 | "name": "Float", 605 | "ofType": null 606 | } 607 | }, 608 | "defaultValue": null, 609 | "isDeprecated": false, 610 | "deprecationReason": null 611 | }, 612 | { 613 | "name": "end", 614 | "description": "", 615 | "type": { 616 | "kind": "NON_NULL", 617 | "name": null, 618 | "ofType": { 619 | "kind": "SCALAR", 620 | "name": "Float", 621 | "ofType": null 622 | } 623 | }, 624 | "defaultValue": null, 625 | "isDeprecated": false, 626 | "deprecationReason": null 627 | } 628 | ], 629 | "type": { 630 | "kind": "NON_NULL", 631 | "name": null, 632 | "ofType": { 633 | "kind": "LIST", 634 | "name": null, 635 | "ofType": { 636 | "kind": "NON_NULL", 637 | "name": null, 638 | "ofType": { 639 | "kind": "OBJECT", 640 | "name": "NodeStatsOverTime", 641 | "ofType": null 642 | } 643 | } 644 | } 645 | }, 646 | "isDeprecated": false, 647 | "deprecationReason": null 648 | }, 649 | { 650 | "name": "getRegionalStats", 651 | "description": "", 652 | "args": [], 653 | "type": { 654 | "kind": "NON_NULL", 655 | "name": null, 656 | "ofType": { 657 | "kind": "OBJECT", 658 | "name": "RegionalStats", 659 | "ofType": null 660 | } 661 | }, 662 | "isDeprecated": false, 663 | "deprecationReason": null 664 | }, 665 | { 666 | "name": "getAltairUpgradePercentage", 667 | "description": "", 668 | "args": [], 669 | "type": { 670 | "kind": "NON_NULL", 671 | "name": null, 672 | "ofType": { 673 | "kind": "SCALAR", 674 | "name": "Float", 675 | "ofType": null 676 | } 677 | }, 678 | "isDeprecated": false, 679 | "deprecationReason": null 680 | } 681 | ], 682 | "inputFields": null, 683 | "interfaces": [], 684 | "enumValues": null, 685 | "possibleTypes": null 686 | }, 687 | { 688 | "kind": "OBJECT", 689 | "name": "RegionalStats", 690 | "description": "", 691 | "specifiedByUrl": null, 692 | "fields": [ 693 | { 694 | "name": "totalParticipatingCountries", 695 | "description": "", 696 | "args": [], 697 | "type": { 698 | "kind": "NON_NULL", 699 | "name": null, 700 | "ofType": { 701 | "kind": "SCALAR", 702 | "name": "Int", 703 | "ofType": null 704 | } 705 | }, 706 | "isDeprecated": false, 707 | "deprecationReason": null 708 | }, 709 | { 710 | "name": "hostedNodePercentage", 711 | "description": "", 712 | "args": [], 713 | "type": { 714 | "kind": "NON_NULL", 715 | "name": null, 716 | "ofType": { 717 | "kind": "SCALAR", 718 | "name": "Float", 719 | "ofType": null 720 | } 721 | }, 722 | "isDeprecated": false, 723 | "deprecationReason": null 724 | }, 725 | { 726 | "name": "nonhostedNodePercentage", 727 | "description": "", 728 | "args": [], 729 | "type": { 730 | "kind": "NON_NULL", 731 | "name": null, 732 | "ofType": { 733 | "kind": "SCALAR", 734 | "name": "Float", 735 | "ofType": null 736 | } 737 | }, 738 | "isDeprecated": false, 739 | "deprecationReason": null 740 | } 741 | ], 742 | "inputFields": null, 743 | "interfaces": [], 744 | "enumValues": null, 745 | "possibleTypes": null 746 | }, 747 | { 748 | "kind": "SCALAR", 749 | "name": "String", 750 | "description": "The `String` scalar type represents textual data, represented as UTF-8 character sequences. The String type is most often used by GraphQL to represent free-form human-readable text.", 751 | "specifiedByUrl": null, 752 | "fields": null, 753 | "inputFields": null, 754 | "interfaces": null, 755 | "enumValues": null, 756 | "possibleTypes": null 757 | }, 758 | { 759 | "kind": "OBJECT", 760 | "name": "__Schema", 761 | "description": "A GraphQL Schema defines the capabilities of a GraphQL server. It exposes all available types and directives on the server, as well as the entry points for query, mutation, and subscription operations.", 762 | "specifiedByUrl": null, 763 | "fields": [ 764 | { 765 | "name": "description", 766 | "description": null, 767 | "args": [], 768 | "type": { 769 | "kind": "SCALAR", 770 | "name": "String", 771 | "ofType": null 772 | }, 773 | "isDeprecated": false, 774 | "deprecationReason": null 775 | }, 776 | { 777 | "name": "types", 778 | "description": "A list of all types supported by this server.", 779 | "args": [], 780 | "type": { 781 | "kind": "NON_NULL", 782 | "name": null, 783 | "ofType": { 784 | "kind": "LIST", 785 | "name": null, 786 | "ofType": { 787 | "kind": "NON_NULL", 788 | "name": null, 789 | "ofType": { 790 | "kind": "OBJECT", 791 | "name": "__Type", 792 | "ofType": null 793 | } 794 | } 795 | } 796 | }, 797 | "isDeprecated": false, 798 | "deprecationReason": null 799 | }, 800 | { 801 | "name": "queryType", 802 | "description": "The type that query operations will be rooted at.", 803 | "args": [], 804 | "type": { 805 | "kind": "NON_NULL", 806 | "name": null, 807 | "ofType": { 808 | "kind": "OBJECT", 809 | "name": "__Type", 810 | "ofType": null 811 | } 812 | }, 813 | "isDeprecated": false, 814 | "deprecationReason": null 815 | }, 816 | { 817 | "name": "mutationType", 818 | "description": "If this server supports mutation, the type that mutation operations will be rooted at.", 819 | "args": [], 820 | "type": { 821 | "kind": "OBJECT", 822 | "name": "__Type", 823 | "ofType": null 824 | }, 825 | "isDeprecated": false, 826 | "deprecationReason": null 827 | }, 828 | { 829 | "name": "subscriptionType", 830 | "description": "If this server support subscription, the type that subscription operations will be rooted at.", 831 | "args": [], 832 | "type": { 833 | "kind": "OBJECT", 834 | "name": "__Type", 835 | "ofType": null 836 | }, 837 | "isDeprecated": false, 838 | "deprecationReason": null 839 | }, 840 | { 841 | "name": "directives", 842 | "description": "A list of all directives supported by this server.", 843 | "args": [], 844 | "type": { 845 | "kind": "NON_NULL", 846 | "name": null, 847 | "ofType": { 848 | "kind": "LIST", 849 | "name": null, 850 | "ofType": { 851 | "kind": "NON_NULL", 852 | "name": null, 853 | "ofType": { 854 | "kind": "OBJECT", 855 | "name": "__Directive", 856 | "ofType": null 857 | } 858 | } 859 | } 860 | }, 861 | "isDeprecated": false, 862 | "deprecationReason": null 863 | } 864 | ], 865 | "inputFields": null, 866 | "interfaces": [], 867 | "enumValues": null, 868 | "possibleTypes": null 869 | }, 870 | { 871 | "kind": "OBJECT", 872 | "name": "__Type", 873 | "description": "The fundamental unit of any GraphQL Schema is the type. There are many kinds of types in GraphQL as represented by the `__TypeKind` enum.\n\nDepending on the kind of a type, certain fields describe information about that type. Scalar types provide no information beyond a name, description and optional `specifiedByUrl`, while Enum types provide their values. Object and Interface types provide the fields they describe. Abstract types, Union and Interface, provide the Object types possible at runtime. List and NonNull types compose other types.", 874 | "specifiedByUrl": null, 875 | "fields": [ 876 | { 877 | "name": "kind", 878 | "description": null, 879 | "args": [], 880 | "type": { 881 | "kind": "NON_NULL", 882 | "name": null, 883 | "ofType": { 884 | "kind": "ENUM", 885 | "name": "__TypeKind", 886 | "ofType": null 887 | } 888 | }, 889 | "isDeprecated": false, 890 | "deprecationReason": null 891 | }, 892 | { 893 | "name": "name", 894 | "description": null, 895 | "args": [], 896 | "type": { 897 | "kind": "SCALAR", 898 | "name": "String", 899 | "ofType": null 900 | }, 901 | "isDeprecated": false, 902 | "deprecationReason": null 903 | }, 904 | { 905 | "name": "description", 906 | "description": null, 907 | "args": [], 908 | "type": { 909 | "kind": "SCALAR", 910 | "name": "String", 911 | "ofType": null 912 | }, 913 | "isDeprecated": false, 914 | "deprecationReason": null 915 | }, 916 | { 917 | "name": "specifiedByUrl", 918 | "description": null, 919 | "args": [], 920 | "type": { 921 | "kind": "SCALAR", 922 | "name": "String", 923 | "ofType": null 924 | }, 925 | "isDeprecated": false, 926 | "deprecationReason": null 927 | }, 928 | { 929 | "name": "fields", 930 | "description": null, 931 | "args": [ 932 | { 933 | "name": "includeDeprecated", 934 | "description": null, 935 | "type": { 936 | "kind": "SCALAR", 937 | "name": "Boolean", 938 | "ofType": null 939 | }, 940 | "defaultValue": "false", 941 | "isDeprecated": false, 942 | "deprecationReason": null 943 | } 944 | ], 945 | "type": { 946 | "kind": "LIST", 947 | "name": null, 948 | "ofType": { 949 | "kind": "NON_NULL", 950 | "name": null, 951 | "ofType": { 952 | "kind": "OBJECT", 953 | "name": "__Field", 954 | "ofType": null 955 | } 956 | } 957 | }, 958 | "isDeprecated": false, 959 | "deprecationReason": null 960 | }, 961 | { 962 | "name": "interfaces", 963 | "description": null, 964 | "args": [], 965 | "type": { 966 | "kind": "LIST", 967 | "name": null, 968 | "ofType": { 969 | "kind": "NON_NULL", 970 | "name": null, 971 | "ofType": { 972 | "kind": "OBJECT", 973 | "name": "__Type", 974 | "ofType": null 975 | } 976 | } 977 | }, 978 | "isDeprecated": false, 979 | "deprecationReason": null 980 | }, 981 | { 982 | "name": "possibleTypes", 983 | "description": null, 984 | "args": [], 985 | "type": { 986 | "kind": "LIST", 987 | "name": null, 988 | "ofType": { 989 | "kind": "NON_NULL", 990 | "name": null, 991 | "ofType": { 992 | "kind": "OBJECT", 993 | "name": "__Type", 994 | "ofType": null 995 | } 996 | } 997 | }, 998 | "isDeprecated": false, 999 | "deprecationReason": null 1000 | }, 1001 | { 1002 | "name": "enumValues", 1003 | "description": null, 1004 | "args": [ 1005 | { 1006 | "name": "includeDeprecated", 1007 | "description": null, 1008 | "type": { 1009 | "kind": "SCALAR", 1010 | "name": "Boolean", 1011 | "ofType": null 1012 | }, 1013 | "defaultValue": "false", 1014 | "isDeprecated": false, 1015 | "deprecationReason": null 1016 | } 1017 | ], 1018 | "type": { 1019 | "kind": "LIST", 1020 | "name": null, 1021 | "ofType": { 1022 | "kind": "NON_NULL", 1023 | "name": null, 1024 | "ofType": { 1025 | "kind": "OBJECT", 1026 | "name": "__EnumValue", 1027 | "ofType": null 1028 | } 1029 | } 1030 | }, 1031 | "isDeprecated": false, 1032 | "deprecationReason": null 1033 | }, 1034 | { 1035 | "name": "inputFields", 1036 | "description": null, 1037 | "args": [ 1038 | { 1039 | "name": "includeDeprecated", 1040 | "description": null, 1041 | "type": { 1042 | "kind": "SCALAR", 1043 | "name": "Boolean", 1044 | "ofType": null 1045 | }, 1046 | "defaultValue": "false", 1047 | "isDeprecated": false, 1048 | "deprecationReason": null 1049 | } 1050 | ], 1051 | "type": { 1052 | "kind": "LIST", 1053 | "name": null, 1054 | "ofType": { 1055 | "kind": "NON_NULL", 1056 | "name": null, 1057 | "ofType": { 1058 | "kind": "OBJECT", 1059 | "name": "__InputValue", 1060 | "ofType": null 1061 | } 1062 | } 1063 | }, 1064 | "isDeprecated": false, 1065 | "deprecationReason": null 1066 | }, 1067 | { 1068 | "name": "ofType", 1069 | "description": null, 1070 | "args": [], 1071 | "type": { 1072 | "kind": "OBJECT", 1073 | "name": "__Type", 1074 | "ofType": null 1075 | }, 1076 | "isDeprecated": false, 1077 | "deprecationReason": null 1078 | } 1079 | ], 1080 | "inputFields": null, 1081 | "interfaces": [], 1082 | "enumValues": null, 1083 | "possibleTypes": null 1084 | }, 1085 | { 1086 | "kind": "ENUM", 1087 | "name": "__TypeKind", 1088 | "description": "An enum describing what kind of type a given `__Type` is.", 1089 | "specifiedByUrl": null, 1090 | "fields": null, 1091 | "inputFields": null, 1092 | "interfaces": null, 1093 | "enumValues": [ 1094 | { 1095 | "name": "SCALAR", 1096 | "description": "Indicates this type is a scalar.", 1097 | "isDeprecated": false, 1098 | "deprecationReason": null 1099 | }, 1100 | { 1101 | "name": "OBJECT", 1102 | "description": "Indicates this type is an object. `fields` and `interfaces` are valid fields.", 1103 | "isDeprecated": false, 1104 | "deprecationReason": null 1105 | }, 1106 | { 1107 | "name": "INTERFACE", 1108 | "description": "Indicates this type is an interface. `fields`, `interfaces`, and `possibleTypes` are valid fields.", 1109 | "isDeprecated": false, 1110 | "deprecationReason": null 1111 | }, 1112 | { 1113 | "name": "UNION", 1114 | "description": "Indicates this type is a union. `possibleTypes` is a valid field.", 1115 | "isDeprecated": false, 1116 | "deprecationReason": null 1117 | }, 1118 | { 1119 | "name": "ENUM", 1120 | "description": "Indicates this type is an enum. `enumValues` is a valid field.", 1121 | "isDeprecated": false, 1122 | "deprecationReason": null 1123 | }, 1124 | { 1125 | "name": "INPUT_OBJECT", 1126 | "description": "Indicates this type is an input object. `inputFields` is a valid field.", 1127 | "isDeprecated": false, 1128 | "deprecationReason": null 1129 | }, 1130 | { 1131 | "name": "LIST", 1132 | "description": "Indicates this type is a list. `ofType` is a valid field.", 1133 | "isDeprecated": false, 1134 | "deprecationReason": null 1135 | }, 1136 | { 1137 | "name": "NON_NULL", 1138 | "description": "Indicates this type is a non-null. `ofType` is a valid field.", 1139 | "isDeprecated": false, 1140 | "deprecationReason": null 1141 | } 1142 | ], 1143 | "possibleTypes": null 1144 | }, 1145 | { 1146 | "kind": "OBJECT", 1147 | "name": "__Field", 1148 | "description": "Object and Interface types are described by a list of Fields, each of which has a name, potentially a list of arguments, and a return type.", 1149 | "specifiedByUrl": null, 1150 | "fields": [ 1151 | { 1152 | "name": "name", 1153 | "description": null, 1154 | "args": [], 1155 | "type": { 1156 | "kind": "NON_NULL", 1157 | "name": null, 1158 | "ofType": { 1159 | "kind": "SCALAR", 1160 | "name": "String", 1161 | "ofType": null 1162 | } 1163 | }, 1164 | "isDeprecated": false, 1165 | "deprecationReason": null 1166 | }, 1167 | { 1168 | "name": "description", 1169 | "description": null, 1170 | "args": [], 1171 | "type": { 1172 | "kind": "SCALAR", 1173 | "name": "String", 1174 | "ofType": null 1175 | }, 1176 | "isDeprecated": false, 1177 | "deprecationReason": null 1178 | }, 1179 | { 1180 | "name": "args", 1181 | "description": null, 1182 | "args": [ 1183 | { 1184 | "name": "includeDeprecated", 1185 | "description": null, 1186 | "type": { 1187 | "kind": "SCALAR", 1188 | "name": "Boolean", 1189 | "ofType": null 1190 | }, 1191 | "defaultValue": "false", 1192 | "isDeprecated": false, 1193 | "deprecationReason": null 1194 | } 1195 | ], 1196 | "type": { 1197 | "kind": "NON_NULL", 1198 | "name": null, 1199 | "ofType": { 1200 | "kind": "LIST", 1201 | "name": null, 1202 | "ofType": { 1203 | "kind": "NON_NULL", 1204 | "name": null, 1205 | "ofType": { 1206 | "kind": "OBJECT", 1207 | "name": "__InputValue", 1208 | "ofType": null 1209 | } 1210 | } 1211 | } 1212 | }, 1213 | "isDeprecated": false, 1214 | "deprecationReason": null 1215 | }, 1216 | { 1217 | "name": "type", 1218 | "description": null, 1219 | "args": [], 1220 | "type": { 1221 | "kind": "NON_NULL", 1222 | "name": null, 1223 | "ofType": { 1224 | "kind": "OBJECT", 1225 | "name": "__Type", 1226 | "ofType": null 1227 | } 1228 | }, 1229 | "isDeprecated": false, 1230 | "deprecationReason": null 1231 | }, 1232 | { 1233 | "name": "isDeprecated", 1234 | "description": null, 1235 | "args": [], 1236 | "type": { 1237 | "kind": "NON_NULL", 1238 | "name": null, 1239 | "ofType": { 1240 | "kind": "SCALAR", 1241 | "name": "Boolean", 1242 | "ofType": null 1243 | } 1244 | }, 1245 | "isDeprecated": false, 1246 | "deprecationReason": null 1247 | }, 1248 | { 1249 | "name": "deprecationReason", 1250 | "description": null, 1251 | "args": [], 1252 | "type": { 1253 | "kind": "SCALAR", 1254 | "name": "String", 1255 | "ofType": null 1256 | }, 1257 | "isDeprecated": false, 1258 | "deprecationReason": null 1259 | } 1260 | ], 1261 | "inputFields": null, 1262 | "interfaces": [], 1263 | "enumValues": null, 1264 | "possibleTypes": null 1265 | }, 1266 | { 1267 | "kind": "OBJECT", 1268 | "name": "__InputValue", 1269 | "description": "Arguments provided to Fields or Directives and the input fields of an InputObject are represented as Input Values which describe their type and optionally a default value.", 1270 | "specifiedByUrl": null, 1271 | "fields": [ 1272 | { 1273 | "name": "name", 1274 | "description": null, 1275 | "args": [], 1276 | "type": { 1277 | "kind": "NON_NULL", 1278 | "name": null, 1279 | "ofType": { 1280 | "kind": "SCALAR", 1281 | "name": "String", 1282 | "ofType": null 1283 | } 1284 | }, 1285 | "isDeprecated": false, 1286 | "deprecationReason": null 1287 | }, 1288 | { 1289 | "name": "description", 1290 | "description": null, 1291 | "args": [], 1292 | "type": { 1293 | "kind": "SCALAR", 1294 | "name": "String", 1295 | "ofType": null 1296 | }, 1297 | "isDeprecated": false, 1298 | "deprecationReason": null 1299 | }, 1300 | { 1301 | "name": "type", 1302 | "description": null, 1303 | "args": [], 1304 | "type": { 1305 | "kind": "NON_NULL", 1306 | "name": null, 1307 | "ofType": { 1308 | "kind": "OBJECT", 1309 | "name": "__Type", 1310 | "ofType": null 1311 | } 1312 | }, 1313 | "isDeprecated": false, 1314 | "deprecationReason": null 1315 | }, 1316 | { 1317 | "name": "defaultValue", 1318 | "description": "A GraphQL-formatted string representing the default value for this input value.", 1319 | "args": [], 1320 | "type": { 1321 | "kind": "SCALAR", 1322 | "name": "String", 1323 | "ofType": null 1324 | }, 1325 | "isDeprecated": false, 1326 | "deprecationReason": null 1327 | }, 1328 | { 1329 | "name": "isDeprecated", 1330 | "description": null, 1331 | "args": [], 1332 | "type": { 1333 | "kind": "NON_NULL", 1334 | "name": null, 1335 | "ofType": { 1336 | "kind": "SCALAR", 1337 | "name": "Boolean", 1338 | "ofType": null 1339 | } 1340 | }, 1341 | "isDeprecated": false, 1342 | "deprecationReason": null 1343 | }, 1344 | { 1345 | "name": "deprecationReason", 1346 | "description": null, 1347 | "args": [], 1348 | "type": { 1349 | "kind": "SCALAR", 1350 | "name": "String", 1351 | "ofType": null 1352 | }, 1353 | "isDeprecated": false, 1354 | "deprecationReason": null 1355 | } 1356 | ], 1357 | "inputFields": null, 1358 | "interfaces": [], 1359 | "enumValues": null, 1360 | "possibleTypes": null 1361 | }, 1362 | { 1363 | "kind": "OBJECT", 1364 | "name": "__EnumValue", 1365 | "description": "One possible value for a given Enum. Enum values are unique values, not a placeholder for a string or numeric value. However an Enum value is returned in a JSON response as a string.", 1366 | "specifiedByUrl": null, 1367 | "fields": [ 1368 | { 1369 | "name": "name", 1370 | "description": null, 1371 | "args": [], 1372 | "type": { 1373 | "kind": "NON_NULL", 1374 | "name": null, 1375 | "ofType": { 1376 | "kind": "SCALAR", 1377 | "name": "String", 1378 | "ofType": null 1379 | } 1380 | }, 1381 | "isDeprecated": false, 1382 | "deprecationReason": null 1383 | }, 1384 | { 1385 | "name": "description", 1386 | "description": null, 1387 | "args": [], 1388 | "type": { 1389 | "kind": "SCALAR", 1390 | "name": "String", 1391 | "ofType": null 1392 | }, 1393 | "isDeprecated": false, 1394 | "deprecationReason": null 1395 | }, 1396 | { 1397 | "name": "isDeprecated", 1398 | "description": null, 1399 | "args": [], 1400 | "type": { 1401 | "kind": "NON_NULL", 1402 | "name": null, 1403 | "ofType": { 1404 | "kind": "SCALAR", 1405 | "name": "Boolean", 1406 | "ofType": null 1407 | } 1408 | }, 1409 | "isDeprecated": false, 1410 | "deprecationReason": null 1411 | }, 1412 | { 1413 | "name": "deprecationReason", 1414 | "description": null, 1415 | "args": [], 1416 | "type": { 1417 | "kind": "SCALAR", 1418 | "name": "String", 1419 | "ofType": null 1420 | }, 1421 | "isDeprecated": false, 1422 | "deprecationReason": null 1423 | } 1424 | ], 1425 | "inputFields": null, 1426 | "interfaces": [], 1427 | "enumValues": null, 1428 | "possibleTypes": null 1429 | }, 1430 | { 1431 | "kind": "OBJECT", 1432 | "name": "__Directive", 1433 | "description": "A Directive provides a way to describe alternate runtime execution and type validation behavior in a GraphQL document.\n\nIn some cases, you need to provide options to alter GraphQL's execution behavior in ways field arguments will not suffice, such as conditionally including or skipping a field. Directives provide this by describing additional information to the executor.", 1434 | "specifiedByUrl": null, 1435 | "fields": [ 1436 | { 1437 | "name": "name", 1438 | "description": null, 1439 | "args": [], 1440 | "type": { 1441 | "kind": "NON_NULL", 1442 | "name": null, 1443 | "ofType": { 1444 | "kind": "SCALAR", 1445 | "name": "String", 1446 | "ofType": null 1447 | } 1448 | }, 1449 | "isDeprecated": false, 1450 | "deprecationReason": null 1451 | }, 1452 | { 1453 | "name": "description", 1454 | "description": null, 1455 | "args": [], 1456 | "type": { 1457 | "kind": "SCALAR", 1458 | "name": "String", 1459 | "ofType": null 1460 | }, 1461 | "isDeprecated": false, 1462 | "deprecationReason": null 1463 | }, 1464 | { 1465 | "name": "isRepeatable", 1466 | "description": null, 1467 | "args": [], 1468 | "type": { 1469 | "kind": "NON_NULL", 1470 | "name": null, 1471 | "ofType": { 1472 | "kind": "SCALAR", 1473 | "name": "Boolean", 1474 | "ofType": null 1475 | } 1476 | }, 1477 | "isDeprecated": false, 1478 | "deprecationReason": null 1479 | }, 1480 | { 1481 | "name": "locations", 1482 | "description": null, 1483 | "args": [], 1484 | "type": { 1485 | "kind": "NON_NULL", 1486 | "name": null, 1487 | "ofType": { 1488 | "kind": "LIST", 1489 | "name": null, 1490 | "ofType": { 1491 | "kind": "NON_NULL", 1492 | "name": null, 1493 | "ofType": { 1494 | "kind": "ENUM", 1495 | "name": "__DirectiveLocation", 1496 | "ofType": null 1497 | } 1498 | } 1499 | } 1500 | }, 1501 | "isDeprecated": false, 1502 | "deprecationReason": null 1503 | }, 1504 | { 1505 | "name": "args", 1506 | "description": null, 1507 | "args": [], 1508 | "type": { 1509 | "kind": "NON_NULL", 1510 | "name": null, 1511 | "ofType": { 1512 | "kind": "LIST", 1513 | "name": null, 1514 | "ofType": { 1515 | "kind": "NON_NULL", 1516 | "name": null, 1517 | "ofType": { 1518 | "kind": "OBJECT", 1519 | "name": "__InputValue", 1520 | "ofType": null 1521 | } 1522 | } 1523 | } 1524 | }, 1525 | "isDeprecated": false, 1526 | "deprecationReason": null 1527 | } 1528 | ], 1529 | "inputFields": null, 1530 | "interfaces": [], 1531 | "enumValues": null, 1532 | "possibleTypes": null 1533 | }, 1534 | { 1535 | "kind": "ENUM", 1536 | "name": "__DirectiveLocation", 1537 | "description": "A Directive can be adjacent to many parts of the GraphQL language, a __DirectiveLocation describes one such possible adjacencies.", 1538 | "specifiedByUrl": null, 1539 | "fields": null, 1540 | "inputFields": null, 1541 | "interfaces": null, 1542 | "enumValues": [ 1543 | { 1544 | "name": "QUERY", 1545 | "description": "Location adjacent to a query operation.", 1546 | "isDeprecated": false, 1547 | "deprecationReason": null 1548 | }, 1549 | { 1550 | "name": "MUTATION", 1551 | "description": "Location adjacent to a mutation operation.", 1552 | "isDeprecated": false, 1553 | "deprecationReason": null 1554 | }, 1555 | { 1556 | "name": "SUBSCRIPTION", 1557 | "description": "Location adjacent to a subscription operation.", 1558 | "isDeprecated": false, 1559 | "deprecationReason": null 1560 | }, 1561 | { 1562 | "name": "FIELD", 1563 | "description": "Location adjacent to a field.", 1564 | "isDeprecated": false, 1565 | "deprecationReason": null 1566 | }, 1567 | { 1568 | "name": "FRAGMENT_DEFINITION", 1569 | "description": "Location adjacent to a fragment definition.", 1570 | "isDeprecated": false, 1571 | "deprecationReason": null 1572 | }, 1573 | { 1574 | "name": "FRAGMENT_SPREAD", 1575 | "description": "Location adjacent to a fragment spread.", 1576 | "isDeprecated": false, 1577 | "deprecationReason": null 1578 | }, 1579 | { 1580 | "name": "INLINE_FRAGMENT", 1581 | "description": "Location adjacent to an inline fragment.", 1582 | "isDeprecated": false, 1583 | "deprecationReason": null 1584 | }, 1585 | { 1586 | "name": "VARIABLE_DEFINITION", 1587 | "description": "Location adjacent to a variable definition.", 1588 | "isDeprecated": false, 1589 | "deprecationReason": null 1590 | }, 1591 | { 1592 | "name": "SCHEMA", 1593 | "description": "Location adjacent to a schema definition.", 1594 | "isDeprecated": false, 1595 | "deprecationReason": null 1596 | }, 1597 | { 1598 | "name": "SCALAR", 1599 | "description": "Location adjacent to a scalar definition.", 1600 | "isDeprecated": false, 1601 | "deprecationReason": null 1602 | }, 1603 | { 1604 | "name": "OBJECT", 1605 | "description": "Location adjacent to an object type definition.", 1606 | "isDeprecated": false, 1607 | "deprecationReason": null 1608 | }, 1609 | { 1610 | "name": "FIELD_DEFINITION", 1611 | "description": "Location adjacent to a field definition.", 1612 | "isDeprecated": false, 1613 | "deprecationReason": null 1614 | }, 1615 | { 1616 | "name": "ARGUMENT_DEFINITION", 1617 | "description": "Location adjacent to an argument definition.", 1618 | "isDeprecated": false, 1619 | "deprecationReason": null 1620 | }, 1621 | { 1622 | "name": "INTERFACE", 1623 | "description": "Location adjacent to an interface definition.", 1624 | "isDeprecated": false, 1625 | "deprecationReason": null 1626 | }, 1627 | { 1628 | "name": "UNION", 1629 | "description": "Location adjacent to a union definition.", 1630 | "isDeprecated": false, 1631 | "deprecationReason": null 1632 | }, 1633 | { 1634 | "name": "ENUM", 1635 | "description": "Location adjacent to an enum definition.", 1636 | "isDeprecated": false, 1637 | "deprecationReason": null 1638 | }, 1639 | { 1640 | "name": "ENUM_VALUE", 1641 | "description": "Location adjacent to an enum value definition.", 1642 | "isDeprecated": false, 1643 | "deprecationReason": null 1644 | }, 1645 | { 1646 | "name": "INPUT_OBJECT", 1647 | "description": "Location adjacent to an input object type definition.", 1648 | "isDeprecated": false, 1649 | "deprecationReason": null 1650 | }, 1651 | { 1652 | "name": "INPUT_FIELD_DEFINITION", 1653 | "description": "Location adjacent to an input object field definition.", 1654 | "isDeprecated": false, 1655 | "deprecationReason": null 1656 | } 1657 | ], 1658 | "possibleTypes": null 1659 | } 1660 | ], 1661 | "directives": [ 1662 | { 1663 | "name": "deprecated", 1664 | "description": "The @deprecated directive is used within the type system definition language to indicate deprecated portions of a GraphQL service’s schema, such as deprecated fields on a type or deprecated enum values.", 1665 | "isRepeatable": false, 1666 | "locations": [ 1667 | "FIELD_DEFINITION", 1668 | "ENUM_VALUE" 1669 | ], 1670 | "args": [ 1671 | { 1672 | "name": "reason", 1673 | "description": "", 1674 | "type": { 1675 | "kind": "SCALAR", 1676 | "name": "String", 1677 | "ofType": null 1678 | }, 1679 | "defaultValue": "\"No longer supported\"", 1680 | "isDeprecated": false, 1681 | "deprecationReason": null 1682 | } 1683 | ] 1684 | }, 1685 | { 1686 | "name": "include", 1687 | "description": "The @include directive may be provided for fields, fragment spreads, and inline fragments, and allows for conditional inclusion during execution as described by the if argument.", 1688 | "isRepeatable": false, 1689 | "locations": [ 1690 | "FIELD", 1691 | "FRAGMENT_SPREAD", 1692 | "INLINE_FRAGMENT" 1693 | ], 1694 | "args": [ 1695 | { 1696 | "name": "if", 1697 | "description": "", 1698 | "type": { 1699 | "kind": "NON_NULL", 1700 | "name": null, 1701 | "ofType": { 1702 | "kind": "SCALAR", 1703 | "name": "Boolean", 1704 | "ofType": null 1705 | } 1706 | }, 1707 | "defaultValue": null, 1708 | "isDeprecated": false, 1709 | "deprecationReason": null 1710 | } 1711 | ] 1712 | }, 1713 | { 1714 | "name": "skip", 1715 | "description": "The @skip directive may be provided for fields, fragment spreads, and inline fragments, and allows for conditional exclusion during execution as described by the if argument.", 1716 | "isRepeatable": false, 1717 | "locations": [ 1718 | "FIELD", 1719 | "FRAGMENT_SPREAD", 1720 | "INLINE_FRAGMENT" 1721 | ], 1722 | "args": [ 1723 | { 1724 | "name": "if", 1725 | "description": "", 1726 | "type": { 1727 | "kind": "NON_NULL", 1728 | "name": null, 1729 | "ofType": { 1730 | "kind": "SCALAR", 1731 | "name": "Boolean", 1732 | "ofType": null 1733 | } 1734 | }, 1735 | "defaultValue": null, 1736 | "isDeprecated": false, 1737 | "deprecationReason": null 1738 | } 1739 | ] 1740 | } 1741 | ] 1742 | } 1743 | } -------------------------------------------------------------------------------- /license-check-and-add-config.json: -------------------------------------------------------------------------------- 1 | { 2 | "ignore": [ 3 | "LICENSE", 4 | "**/*.json", 5 | "**/*.txt", 6 | "**/*.md", 7 | ".github", 8 | "*.lock", 9 | ".env", 10 | ".env.template", 11 | "build", 12 | "src/assets" 13 | ], 14 | "ignoreFile": ".gitignore", 15 | "license": "copyright.txt", 16 | "licenseFormats": { 17 | "gitignore|npmignore|eslintignore|dockerignore|sh|py": { 18 | "eachLine": { 19 | "prepend": "# " 20 | } 21 | }, 22 | "html|xml|svg": { 23 | "prepend": "" 25 | }, 26 | "js|ts|css|scss": { 27 | "prepend": "/*", 28 | "append": "*/" 29 | } 30 | }, 31 | "trailingWhitespace": "TRIM" 32 | } 33 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "eth2-crawler-ui", 3 | "version": "0.1.0", 4 | "private": true, 5 | "engines": { 6 | "node": "14.x" 7 | }, 8 | "dependencies": { 9 | "@apollo/client": "^3.3.21", 10 | "@chainsafe/common-components": "^1.0.29", 11 | "@chainsafe/common-theme": "^1.0.10", 12 | "@craco/craco": "^6.2.0", 13 | "@testing-library/jest-dom": "^5.11.4", 14 | "@testing-library/react": "^11.1.0", 15 | "@testing-library/user-event": "^12.1.10", 16 | "@types/jest": "^26.0.15", 17 | "@types/node": "^12.0.0", 18 | "@types/react": "^17.0.0", 19 | "@types/react-dom": "^17.0.0", 20 | "apollo-boost": "^0.4.9", 21 | "chart.js": "^3.4.1", 22 | "clsx": "^1.1.1", 23 | "craco-babel-loader": "^0.1.4", 24 | "dayjs": "^1.10.6", 25 | "formik": "^2.2.9", 26 | "graphql": "^15.5.1", 27 | "graphql-request": "^3.4.0", 28 | "leaflet": "^1.7.1", 29 | "leaflet.heat": "^0.2.0", 30 | "react": "^17.0.2", 31 | "react-chartjs-2": "^3.0.3", 32 | "react-dom": "^17.0.2", 33 | "react-leaflet": "^3.2.0", 34 | "react-scripts": "4.0.3", 35 | "react-toast-notifications": "^2.5.1", 36 | "react-tooltip": "^4.2.21", 37 | "recharts": "^2.0.10", 38 | "simpleheat": "^0.4.0", 39 | "typescript": "^4.1.2", 40 | "web-vitals": "^1.0.1" 41 | }, 42 | "scripts": { 43 | "start": "craco start", 44 | "build": "craco build", 45 | "test": "craco test", 46 | "lint": "eslint './src/**/*.{ts,tsx}'", 47 | "lint:fix": "eslint --fix './src/**/*.{ts,tsx}'", 48 | "license-add": "license-check-and-add add -f license-check-and-add-config.json", 49 | "license-check": "license-check-and-add check -f license-check-and-add-config.json", 50 | "license-remove": "license-check-and-add remove -f license-check-and-add-config.json", 51 | "get-graph-schema": "yarn apollo schema:download --endpoint=https://crawler.imploy.site/query graphql-schema.json", 52 | "generate-graph-types": "yarn apollo codegen:generate --localSchemaFile=graphql-schema.json --target=typescript --includes=src/**/*.ts --tagName=gql --addTypename --globalTypesFile=src/types/graphql-global-types.ts types", 53 | "download-and-generate-graph-types": "yarn get-graph-schema && yarn generate-graph-types" 54 | }, 55 | "eslintConfig": { 56 | "extends": [ 57 | "react-app", 58 | "react-app/jest" 59 | ] 60 | }, 61 | "browserslist": { 62 | "production": [ 63 | ">0.2%", 64 | "not dead", 65 | "not op_mini all" 66 | ], 67 | "development": [ 68 | "last 1 chrome version", 69 | "last 1 firefox version", 70 | "last 1 safari version" 71 | ] 72 | }, 73 | "devDependencies": { 74 | "@types/graphql": "^14.5.0", 75 | "@types/leaflet": "^1.7.4", 76 | "apollo": "^2.33.4", 77 | "eslint-config-prettier": "^8.3.0", 78 | "eslint-plugin-prettier": "^3.4.0", 79 | "eslint-plugin-react-hooks": "^4.2.0", 80 | "license-check-and-add": "^4.0.2", 81 | "prettier": "^2.3.2" 82 | } 83 | } 84 | -------------------------------------------------------------------------------- /public/icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ChainSafe/nodewatch-ui/423a1184c5961462f98497c87aed21934fb5f03e/public/icon.png -------------------------------------------------------------------------------- /public/index.html: -------------------------------------------------------------------------------- 1 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 21 | Nodewatch - Eth2 Node Analytics 22 | 23 | 24 | 25 | 26 |
27 | 37 | 38 | 39 | -------------------------------------------------------------------------------- /public/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "short_name": "Eth 2 Crawler", 3 | "name": "Eth 2 Crawler", 4 | "icons": [ 5 | { 6 | "src": "favicon.ico", 7 | "sizes": "64x64 32x32 24x24 16x16", 8 | "type": "image/x-icon" 9 | } 10 | ], 11 | "start_url": ".", 12 | "display": "standalone", 13 | "theme_color": "#000000", 14 | "background_color": "#ffffff" 15 | } 16 | -------------------------------------------------------------------------------- /public/robots.txt: -------------------------------------------------------------------------------- 1 | # https://www.robotstxt.org/robotstxt.html 2 | User-agent: * 3 | Disallow: 4 | -------------------------------------------------------------------------------- /src/App.tsx: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2021 ChainSafe Systems 3 | SPDX-License-Identifier: LGPL-3.0-only 4 | */ 5 | import React from "react" 6 | import HomePage from "./Components/Pages/HomePage" 7 | import { ThemeSwitcher, createStyles, makeStyles } from "@chainsafe/common-theme" 8 | import { theme } from "./Components/Themes/theme" 9 | import { Eth2CrawlerProvider } from "./Contexts/Eth2CrawlerContext" 10 | import BodyLayout from "./Components/Layouts/BodyLayout" 11 | import NavBar from "./Components/Modules/Navbar" 12 | import Footer from "./Components/Modules/Footer" 13 | 14 | const useStyles = makeStyles(() => { 15 | return createStyles({ 16 | root: { 17 | backgroundColor: "#131825", 18 | }, 19 | }) 20 | }) 21 | 22 | function App() { 23 | const classes = useStyles() 24 | return ( 25 | 26 | 27 |
28 | 29 | 30 | 31 | 32 |
33 |
34 |
35 |
36 | ) 37 | } 38 | 39 | export default App 40 | -------------------------------------------------------------------------------- /src/Components/Elements/Icons/ToolTipIcon.tsx: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2021 ChainSafe Systems 3 | SPDX-License-Identifier: LGPL-3.0-only 4 | */ 5 | import React from "react" 6 | 7 | function ToolTipIcon(props: any) { 8 | return ( 9 | 16 | 20 | 24 | 25 | ) 26 | } 27 | 28 | export default ToolTipIcon 29 | -------------------------------------------------------------------------------- /src/Components/Elements/Pagination.tsx: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2021 ChainSafe Systems 3 | SPDX-License-Identifier: LGPL-3.0-only 4 | */ 5 | import React from "react" 6 | import { createStyles, makeStyles } from "@chainsafe/common-theme" 7 | import { ArrowLeftIcon, ArrowRightIcon, Typography } from "@chainsafe/common-components" 8 | import { ECTheme } from "../Themes/types" 9 | import clsx from "clsx" 10 | 11 | const useStyles = makeStyles(({ constants, palette }: ECTheme) => { 12 | return createStyles({ 13 | root: { 14 | display: "flex", 15 | color: palette.text.primary, 16 | alignItems: "center", 17 | }, 18 | icons: { 19 | fill: palette.text.primary, 20 | fontSize: 12, 21 | cursor: "pointer", 22 | padding: constants.generalUnit, 23 | }, 24 | leftIcon: { 25 | marginRight: constants.generalUnit, 26 | }, 27 | rightIcon: { 28 | marginLeft: constants.generalUnit, 29 | }, 30 | }) 31 | }) 32 | 33 | interface IPaginationProps { 34 | pageNo: number 35 | totalPages: number 36 | onNextPage?: () => void 37 | onPreviousPage?: () => void 38 | } 39 | 40 | const Pagination: React.FC = ({ 41 | pageNo, 42 | totalPages, 43 | onNextPage, 44 | onPreviousPage, 45 | }) => { 46 | const classes = useStyles() 47 | 48 | return ( 49 |
50 | 55 | 56 | Page {pageNo} of {totalPages} 57 | 58 | 63 |
64 | ) 65 | } 66 | 67 | export { Pagination } 68 | -------------------------------------------------------------------------------- /src/Components/Elements/StatTitles.tsx: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2021 ChainSafe Systems 3 | SPDX-License-Identifier: LGPL-3.0-only 4 | */ 5 | import React from "react" 6 | import { createStyles, makeStyles } from "@chainsafe/common-theme" 7 | import { Typography } from "@chainsafe/common-components" 8 | import { ECTheme } from "../Themes/types" 9 | 10 | const useStyles = makeStyles(({ palette, constants }: ECTheme) => { 11 | return createStyles({ 12 | title: { 13 | fontSize: "32px", 14 | lineHeight: "40px", 15 | color: palette.additional["gray"][2], 16 | }, 17 | line: { 18 | width: 24, 19 | backgroundColor: palette.primary.main, 20 | height: 2, 21 | marginBottom: constants.generalUnit * 0.5, 22 | }, 23 | subtitle: { 24 | fontSize: "16px", 25 | lineHeight: "20px", 26 | color: palette.additional["gray"][2], 27 | marginBottom: constants.generalUnit * 2, 28 | }, 29 | }) 30 | }) 31 | 32 | const StatTitleLarge: React.FC = ({ children }) => { 33 | const classes = useStyles() 34 | 35 | return ( 36 |
37 | 38 | {children} 39 | 40 |
41 |
42 | ) 43 | } 44 | 45 | const StatSubTitle: React.FC = ({ children }) => { 46 | const classes = useStyles() 47 | 48 | return ( 49 | 50 | {children} 51 | 52 | ) 53 | } 54 | 55 | export { StatTitleLarge, StatSubTitle } 56 | -------------------------------------------------------------------------------- /src/Components/Layouts/BodyLayout.tsx: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2021 ChainSafe Systems 3 | SPDX-License-Identifier: LGPL-3.0-only 4 | */ 5 | import React from "react" 6 | import { createStyles, makeStyles } from "@chainsafe/common-theme" 7 | import { ECTheme } from "../Themes/types" 8 | const useStyles = makeStyles(({ breakpoints, palette }: ECTheme) => { 9 | return createStyles({ 10 | layout: { 11 | background: palette.background.default, 12 | fontFamily: "Neue Montreal", 13 | margin: "0 auto", 14 | display: "flex", 15 | flexDirection: "column", 16 | justifyContent: "center", 17 | maxWidth: breakpoints.values["lg"], 18 | }, 19 | }) 20 | }) 21 | 22 | const BodyLayout: React.FC = ({ children }) => { 23 | const classes = useStyles() 24 | return
{children}
25 | } 26 | 27 | export default BodyLayout 28 | -------------------------------------------------------------------------------- /src/Components/Layouts/GridLayout/GridLayoutWrapper.tsx: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2021 ChainSafe Systems 3 | SPDX-License-Identifier: LGPL-3.0-only 4 | */ 5 | import React, { ReactNode } from "react" 6 | import { createStyles, makeStyles } from "@chainsafe/common-theme" 7 | import { Typography } from "@chainsafe/common-components" 8 | import clsx from "clsx" 9 | import { ECTheme } from "../../Themes/types" 10 | 11 | 12 | const useStyles = makeStyles(({ constants, palette, }: ECTheme) => { 13 | return createStyles({ 14 | root: { 15 | marginBottom: constants.generalUnit * 4, 16 | }, 17 | heading: { 18 | marginBottom: constants.generalUnit * 3, 19 | color: palette.text.primary, 20 | }, 21 | }) 22 | }) 23 | 24 | interface IGridLayoutWrapper { 25 | className?: string 26 | heading: string 27 | children: ReactNode | ReactNode[] 28 | } 29 | 30 | const GridLayoutWrapper = ({ className, heading, children }: IGridLayoutWrapper) => { 31 | const classes = useStyles() 32 | 33 | return (
34 | 35 | {heading} 36 | 37 |
38 | {children} 39 |
40 |
) 41 | } 42 | 43 | export default GridLayoutWrapper 44 | -------------------------------------------------------------------------------- /src/Components/Layouts/SectionTile/CardStat.tsx: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2021 ChainSafe Systems 3 | SPDX-License-Identifier: LGPL-3.0-only 4 | */ 5 | import React from "react" 6 | import { createStyles, makeStyles } from "@chainsafe/common-theme" 7 | import { ECTheme } from "../../Themes/types" 8 | import clsx from "clsx" 9 | import { Typography } from "@chainsafe/common-components" 10 | import ToolTipIcon from "../../Elements/Icons/ToolTipIcon" 11 | import ReactTooltip from "react-tooltip" 12 | 13 | const useStyles = makeStyles(({ constants, palette }: ECTheme) => { 14 | return createStyles({ 15 | root: { 16 | marginBottom: constants.generalUnit * 6, 17 | }, 18 | heading: { 19 | color: palette.additional["gray"][2], 20 | "&.red": { 21 | color: constants.statColors.red, 22 | }, 23 | "&.blue": { 24 | color: constants.statColors.blue, 25 | }, 26 | "&.green": { 27 | color: constants.statColors.green, 28 | }, 29 | }, 30 | statColor: { 31 | "&.red": { 32 | color: constants.statColors.red, 33 | }, 34 | "&.blue": { 35 | color: constants.statColors.blue, 36 | }, 37 | "&.green": { 38 | color: constants.statColors.green, 39 | }, 40 | }, 41 | headingContainer: { 42 | display: "flex", 43 | alignItems: "center", 44 | }, 45 | containerMargin: { 46 | marginBottom: constants.generalUnit * 1.5, 47 | }, 48 | tooltipIcon: { 49 | width: 16, 50 | height: 16, 51 | marginLeft: constants.generalUnit, 52 | }, 53 | }) 54 | }) 55 | 56 | export interface ISectionCard { 57 | heading: string 58 | stat: string 59 | className?: string 60 | isGreen?: boolean 61 | isRed?: boolean 62 | isBlue?: boolean 63 | tooltip?: React.ReactChild 64 | tooltipId?: string 65 | } 66 | 67 | const CardStat = ({ 68 | className, 69 | heading, 70 | stat, 71 | isGreen, 72 | isBlue, 73 | isRed, 74 | tooltip, 75 | tooltipId, 76 | }: ISectionCard) => { 77 | const classes = useStyles() 78 | 79 | return ( 80 |
81 |
82 |
83 | 88 | {heading} 89 | 90 | {tooltip && tooltipId && ( 91 | <> 92 | 93 | 94 | {tooltip} 95 | 96 | 97 | )} 98 |
99 |
100 | 101 | 106 | {stat} 107 | 108 |
109 | ) 110 | } 111 | 112 | export default CardStat 113 | -------------------------------------------------------------------------------- /src/Components/Layouts/SectionTile/SectionBody.tsx: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2021 ChainSafe Systems 3 | SPDX-License-Identifier: LGPL-3.0-only 4 | */ 5 | import React, { ReactNode } from "react" 6 | import { createStyles, makeStyles } from "@chainsafe/common-theme" 7 | import { ECTheme } from "../../Themes/types" 8 | import clsx from "clsx" 9 | 10 | const useStyles = makeStyles(({ palette, constants }: ECTheme) => { 11 | return createStyles({ 12 | root: { 13 | border: `1px solid ${palette.background.paper}`, 14 | borderRadius: constants.generalUnit / 2, 15 | flex: "1 1 0", 16 | }, 17 | }) 18 | }) 19 | 20 | export interface ISectionCard { 21 | children: ReactNode | ReactNode[] 22 | className?: string 23 | } 24 | 25 | const SectionBody = ({ children, className }: ISectionCard) => { 26 | const classes = useStyles() 27 | 28 | return
{children}
29 | } 30 | 31 | export default SectionBody 32 | -------------------------------------------------------------------------------- /src/Components/Layouts/SectionTile/SectionCard.tsx: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2021 ChainSafe Systems 3 | SPDX-License-Identifier: LGPL-3.0-only 4 | */ 5 | import React, { ReactNode } from "react" 6 | import { createStyles, makeStyles } from "@chainsafe/common-theme" 7 | import clsx from "clsx" 8 | import { ECTheme } from "../../Themes/types" 9 | 10 | const useStyles = makeStyles(({ constants, palette, breakpoints }: ECTheme) => { 11 | return createStyles({ 12 | root: { 13 | border: `1px solid ${palette.background.paper}`, 14 | padding: constants.generalUnit * 2, 15 | borderRadius: constants.generalUnit / 2, 16 | display: "flex", 17 | flexDirection: "column", 18 | justifyContent: "space-between", 19 | color: palette.additional["gray"][2], 20 | "& > *:last-child": { 21 | marginBottom: 0, 22 | }, 23 | [breakpoints.up("md")]: { 24 | width: "30%", 25 | marginRight: constants.generalUnit * 3, 26 | }, 27 | [breakpoints.down("md")]: { 28 | marginBottom: constants.generalUnit * 3, 29 | }, 30 | }, 31 | }) 32 | }) 33 | 34 | export interface ISectionCard { 35 | children: ReactNode | ReactNode[] 36 | className?: string 37 | } 38 | 39 | const SectionCard = ({ children, className }: ISectionCard) => { 40 | const classes = useStyles() 41 | 42 | return
{children}
43 | } 44 | 45 | export default SectionCard 46 | -------------------------------------------------------------------------------- /src/Components/Layouts/SectionTile/SectionTile.tsx: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2021 ChainSafe Systems 3 | SPDX-License-Identifier: LGPL-3.0-only 4 | */ 5 | import React, { ReactNode } from "react" 6 | import { createStyles, makeStyles } from "@chainsafe/common-theme" 7 | import { Typography } from "@chainsafe/common-components" 8 | import SectionCard from "./SectionCard" 9 | import SectionBody from "./SectionBody" 10 | import clsx from "clsx" 11 | import { ECTheme } from "../../Themes/types" 12 | 13 | const useStyles = makeStyles(({ constants, breakpoints, palette }: ECTheme) => { 14 | return createStyles({ 15 | root: { 16 | marginBottom: constants.generalUnit * 6, 17 | }, 18 | heading: { 19 | marginBottom: constants.generalUnit * 3, 20 | color: palette.text.primary, 21 | }, 22 | content: { 23 | display: "flex", 24 | flexDirection: "row", 25 | justifyContent: "flex-start", 26 | [breakpoints.down("md")]: { 27 | flexDirection: "column", 28 | }, 29 | }, 30 | }) 31 | }) 32 | 33 | interface ISectionTile { 34 | className?: string 35 | heading: string 36 | cardContent: ReactNode 37 | children: ReactNode | ReactNode[] 38 | } 39 | 40 | const SectionTile = ({ className, heading, cardContent, children }: ISectionTile) => { 41 | const classes = useStyles() 42 | 43 | return ( 44 |
45 | 46 | {heading} 47 | 48 |
49 | {cardContent} 50 | {children} 51 |
52 |
53 | ) 54 | } 55 | 56 | export default SectionTile 57 | -------------------------------------------------------------------------------- /src/Components/Modules/CountryStats/CountryBox.tsx: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2021 ChainSafe Systems 3 | SPDX-License-Identifier: LGPL-3.0-only 4 | */ 5 | import React from "react" 6 | import { createStyles, makeStyles } from "@chainsafe/common-theme" 7 | import { ECTheme } from "../../Themes/types" 8 | import clsx from "clsx" 9 | import { Typography } from "@chainsafe/common-components" 10 | 11 | const useStyles = makeStyles(({ palette, constants }: ECTheme) => { 12 | return createStyles({ 13 | root: { 14 | border: `1px solid ${palette.background.paper}`, 15 | borderRadius: "3px", 16 | padding: constants.generalUnit * 2, 17 | width: "inherit", 18 | height: "inherit", 19 | }, 20 | countryTitle: { 21 | marginRight: constants.generalUnit * 6, 22 | }, 23 | countRow: { 24 | display: "flex", 25 | justifyContent: "space-between", 26 | color: palette.text.primary, 27 | margin: `${constants.generalUnit * 2}px 0`, 28 | }, 29 | }) 30 | }) 31 | 32 | interface ICountryBoxProps { 33 | countries: { 34 | rank: number 35 | name: string 36 | count: number 37 | percentage: string 38 | }[] 39 | className?: string 40 | } 41 | 42 | const CountryBox: React.FC = ({ countries, className }) => { 43 | const classes = useStyles() 44 | return ( 45 |
46 | {countries.map((country, i) => ( 47 |
48 | 49 | {country.rank}. {country.name} 50 | 51 | 52 | {country.count} ({country.percentage}%) 53 | 54 |
55 | ))} 56 |
57 | ) 58 | } 59 | 60 | export default CountryBox 61 | -------------------------------------------------------------------------------- /src/Components/Modules/CountryStats/StatsChartBox.tsx: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2021 ChainSafe Systems 3 | SPDX-License-Identifier: LGPL-3.0-only 4 | */ 5 | import React, { useState } from "react" 6 | import { createStyles, makeStyles, useTheme } from "@chainsafe/common-theme" 7 | import { ECTheme } from "../../Themes/types" 8 | import { PieChart, Pie, Sector, ResponsiveContainer } from "recharts" 9 | 10 | const useStyles = makeStyles(({ constants }: ECTheme) => { 11 | return createStyles({ 12 | root: { 13 | display: "flex", 14 | flexDirection: "column", 15 | justifyContent: "center", 16 | flex: 1, 17 | }, 18 | chartContainer: { 19 | height: `${constants.chartSizes.chartHeight}px`, 20 | }, 21 | }) 22 | }) 23 | 24 | interface IStatsChartBoxProps { 25 | countries: { 26 | rank: number 27 | name: string 28 | count: number 29 | percentage: string 30 | }[] 31 | } 32 | 33 | const renderActiveShape = (props: any, fill: string) => { 34 | const { cx, cy, innerRadius, outerRadius, startAngle, endAngle, payload } = props 35 | 36 | return ( 37 | 38 | 39 | {payload.name} 40 | 41 | 50 | 59 | 60 | ) 61 | } 62 | 63 | const CountryBox: React.FC = ({ countries }) => { 64 | const classes = useStyles() 65 | const theme: ECTheme = useTheme() 66 | const [activeIndex, setActiveIndex] = useState(0) 67 | 68 | const onPieEnter = (_: any, index: number) => { 69 | setActiveIndex(index) 70 | } 71 | 72 | const data = countries.map((country) => ({ 73 | name: `${country.name}(${country.percentage}%)`, 74 | value: country.count, 75 | })) 76 | 77 | return ( 78 |
79 |
80 | 81 | 82 | 85 | renderActiveShape(props, theme.constants.chartPrimaryColors.main) 86 | } 87 | data={data} 88 | cx="50%" 89 | cy="50%" 90 | innerRadius={110} 91 | outerRadius={130} 92 | fill={theme.constants.chartPrimaryColors.main} 93 | dataKey="value" 94 | onMouseEnter={onPieEnter} 95 | /> 96 | 97 | 98 |
99 |
100 | ) 101 | } 102 | 103 | export default CountryBox 104 | -------------------------------------------------------------------------------- /src/Components/Modules/CountryStats/index.tsx: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2021 ChainSafe Systems 3 | SPDX-License-Identifier: LGPL-3.0-only 4 | */ 5 | import React, { useCallback, useEffect, useMemo, useState } from "react" 6 | import { createStyles, makeStyles, useTheme } from "@chainsafe/common-theme" 7 | import { ECTheme } from "../../Themes/types" 8 | import { useEth2CrawlerApi } from "../../../Contexts/Eth2CrawlerContext" 9 | import CountryBox from "./CountryBox" 10 | import StatsChartBox from "./StatsChartBox" 11 | import { Typography } from "@chainsafe/common-components" 12 | import { Pagination } from "../../Elements/Pagination" 13 | import useWindowDimensions from "../../../utilHooks/useWindowDimensions" 14 | 15 | const useStyles = makeStyles(({ palette, constants, breakpoints }: ECTheme) => { 16 | return createStyles({ 17 | root: { 18 | marginBottom: constants.generalUnit * 8, 19 | }, 20 | container: { 21 | display: "grid", 22 | gridTemplateColumns: "2fr 1fr", 23 | gridColumnGap: constants.generalUnit * 4, 24 | minHeight: 430, 25 | [breakpoints.down("md")]: { 26 | gridTemplateColumns: "1fr", 27 | gridRowGap: constants.generalUnit * 2, 28 | }, 29 | [breakpoints.down("sm")]: { 30 | gridTemplateColumns: "1fr", 31 | }, 32 | }, 33 | countriesContainer: { 34 | display: "grid", 35 | gridTemplateColumns: "1fr 1fr", 36 | gridColumnGap: constants.generalUnit * 4, 37 | minHeight: 430, 38 | [breakpoints.down("md")]: { 39 | gridTemplateColumns: "1fr 1fr", 40 | gridRowGap: constants.generalUnit * 4, 41 | }, 42 | [breakpoints.down("sm")]: { 43 | gridTemplateColumns: "1fr", 44 | }, 45 | }, 46 | countryBox1: { 47 | flex: 1, 48 | }, 49 | countryBox2: { 50 | flex: 1, 51 | }, 52 | title: { 53 | marginBottom: constants.generalUnit * 3, 54 | color: palette.text.primary, 55 | }, 56 | pagination: { 57 | marginTop: constants.generalUnit * 2, 58 | [breakpoints.down("md")]: { 59 | marginTop: 0, 60 | marginBottom: constants.generalUnit * 4, 61 | }, 62 | [breakpoints.down("sm")]: { 63 | marginBottom: constants.generalUnit * 4, 64 | }, 65 | }, 66 | }) 67 | }) 68 | 69 | const PAGE_SIZE = 20 70 | const HALF_PAGE_SIZE = PAGE_SIZE / 2 71 | 72 | const CountryStats: React.FC = () => { 73 | const classes = useStyles() 74 | const { nodeCountByCountries } = useEth2CrawlerApi() 75 | const [pageNo, setPageNo] = useState(0) 76 | 77 | const { width } = useWindowDimensions() 78 | const theme: ECTheme = useTheme() 79 | const isDesktop = width > theme.breakpoints.values["md"] 80 | const isTab = width > theme.breakpoints.values["sm"] && width < theme.breakpoints.values["md"] 81 | 82 | const showTwoCountryBoxes = isDesktop || isTab 83 | 84 | useEffect(() => { 85 | if (nodeCountByCountries.length) { 86 | setPageNo(1) 87 | } 88 | }, [nodeCountByCountries, isDesktop]) 89 | 90 | const totalPages = useMemo( 91 | () => Math.ceil(nodeCountByCountries.length / (isDesktop ? PAGE_SIZE : HALF_PAGE_SIZE)), 92 | [nodeCountByCountries, isDesktop] 93 | ) 94 | 95 | const onPrevPage = useCallback(() => { 96 | if (pageNo > 1) { 97 | setPageNo(pageNo - 1) 98 | } 99 | }, [pageNo]) 100 | 101 | const onNextPage = useCallback(() => { 102 | if (pageNo < totalPages) { 103 | setPageNo(pageNo + 1) 104 | } 105 | }, [pageNo, totalPages]) 106 | 107 | const totalNodeCount = useMemo( 108 | () => 109 | nodeCountByCountries.reduce((total, item) => { 110 | total += item.count 111 | return total 112 | }, 0), 113 | [nodeCountByCountries] 114 | ) 115 | 116 | const sortedNodeCountByCountries = useMemo( 117 | () => 118 | nodeCountByCountries 119 | .sort((a, b) => (a.count < b.count ? 1 : -1)) 120 | .map((nodeByCountry, i) => ({ 121 | ...nodeByCountry, 122 | rank: i + 1, 123 | percentage: ((nodeByCountry.count / totalNodeCount) * 100).toFixed(2), 124 | })), 125 | [nodeCountByCountries, totalNodeCount] 126 | ) 127 | 128 | const first10Countries = sortedNodeCountByCountries.slice( 129 | (pageNo - 1) * (showTwoCountryBoxes ? PAGE_SIZE : HALF_PAGE_SIZE), 130 | showTwoCountryBoxes ? pageNo * PAGE_SIZE - HALF_PAGE_SIZE : pageNo * HALF_PAGE_SIZE 131 | ) 132 | const second10Countries = sortedNodeCountByCountries.slice( 133 | pageNo * PAGE_SIZE - HALF_PAGE_SIZE, 134 | pageNo * PAGE_SIZE 135 | ) 136 | 137 | return ( 138 |
139 | 140 | Node count by countries 141 | 142 |
143 |
144 | 145 | {showTwoCountryBoxes && ( 146 | 147 | )} 148 |
149 | {(!showTwoCountryBoxes || isTab) && ( 150 |
151 | 157 |
158 | )} 159 | 160 |
161 | {pageNo > 0 && showTwoCountryBoxes && !isTab && ( 162 |
163 | 169 |
170 | )} 171 |
172 | ) 173 | } 174 | 175 | export default CountryStats 176 | -------------------------------------------------------------------------------- /src/Components/Modules/DemographicsStats/ClientTypes.tsx: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2021 ChainSafe Systems 3 | SPDX-License-Identifier: LGPL-3.0-only 4 | */ 5 | import React, { useMemo } from "react" 6 | import { createStyles, makeStyles, useTheme } from "@chainsafe/common-theme" 7 | import { Typography } from "@chainsafe/common-components" 8 | import { useEth2CrawlerApi } from "../../../Contexts/Eth2CrawlerContext" 9 | import { ECTheme } from "../../Themes/types" 10 | import { BarChart, Bar, Tooltip, XAxis, YAxis, ResponsiveContainer } from "recharts" 11 | 12 | const useStyles = makeStyles(({ palette, constants }: ECTheme) => { 13 | return createStyles({ 14 | root: { 15 | border: `1px solid ${palette.background.paper}`, 16 | borderRadius: "3px", 17 | padding: constants.generalUnit * 2, 18 | width: "inherit", 19 | height: "inherit", 20 | }, 21 | chartContainer: { 22 | height: `${constants.chartSizes.chartHeight}px`, 23 | }, 24 | title: { 25 | marginBottom: constants.generalUnit * 2, 26 | color: palette.text.primary, 27 | }, 28 | }) 29 | }) 30 | 31 | // const MIN_CLIENT_COUNT = 20 32 | 33 | const ClientTypes = () => { 34 | const classes = useStyles() 35 | const theme: ECTheme = useTheme() 36 | 37 | const { clients } = useEth2CrawlerApi() 38 | 39 | const chartData = useMemo( 40 | () => 41 | clients 42 | .sort((first, second) => (first.count > second.count ? 1 : -1)) 43 | // .filter((client) => client.count > MIN_CLIENT_COUNT) 44 | .map((client) => ({ 45 | name: client.name || "unknown", 46 | count: client.count, 47 | })), 48 | [clients] 49 | ) 50 | 51 | return ( 52 |
53 | 54 | Client type distribution 55 | 56 |
57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 |
66 |
67 | ) 68 | } 69 | 70 | export default ClientTypes 71 | -------------------------------------------------------------------------------- /src/Components/Modules/DemographicsStats/NodeCount12.tsx: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2021 ChainSafe Systems 3 | SPDX-License-Identifier: LGPL-3.0-only 4 | */ 5 | import React from "react" 6 | import { createStyles, makeStyles, useTheme } from "@chainsafe/common-theme" 7 | import { Line } from "react-chartjs-2" 8 | import { Typography } from "@chainsafe/common-components" 9 | import { ECTheme } from "../../Themes/types" 10 | 11 | const useStyles = makeStyles(({ palette, constants }: ECTheme) => { 12 | return createStyles({ 13 | root: { 14 | border: `1px solid ${palette.additional["gray"][4]}`, 15 | borderRadius: "3px", 16 | padding: constants.generalUnit * 2, 17 | }, 18 | title: { 19 | marginBottom: constants.generalUnit * 4, 20 | }, 21 | }) 22 | }) 23 | 24 | const NodeCount12 = () => { 25 | const classes = useStyles() 26 | 27 | const theme: ECTheme = useTheme() 28 | 29 | const data = { 30 | labels: ["1", "2", "3", "4", "5", "6", "7"], 31 | datasets: [ 32 | { 33 | label: "Node count: eth1", 34 | data: [65, 59, 80, 81, 56, 55, 40], 35 | fill: true, 36 | borderColor: theme.palette.primary.main, 37 | backgroundColor: theme.palette.primary.background, 38 | tension: 0.1, 39 | }, 40 | { 41 | label: "Node count: eth2", 42 | data: [99, 56, 55, 40, 65, 59, 100], 43 | fill: true, 44 | borderColor: theme.palette.primary.main, 45 | backgroundColor: theme.palette.primary.background, 46 | tension: 0.1, 47 | }, 48 | ], 49 | } 50 | 51 | const options = { 52 | scales: { 53 | y: { 54 | display: false, 55 | }, 56 | x: { 57 | grid: { 58 | display: false, 59 | }, 60 | }, 61 | }, 62 | plugins: { 63 | legend: { 64 | display: false, 65 | }, 66 | }, 67 | } 68 | 69 | return ( 70 |
71 | 72 | node count eth1 and eth2 73 | 74 |
75 | 76 |
77 |
78 | ) 79 | } 80 | 81 | export default NodeCount12 82 | -------------------------------------------------------------------------------- /src/Components/Modules/DemographicsStats/NodeReadyForFork.tsx: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2021 ChainSafe Systems 3 | SPDX-License-Identifier: LGPL-3.0-only 4 | */ 5 | import React from "react" 6 | import { createStyles, makeStyles, useTheme } from "@chainsafe/common-theme" 7 | import { Line } from "react-chartjs-2" 8 | import { Typography } from "@chainsafe/common-components" 9 | import { ECTheme } from "../../Themes/types" 10 | 11 | const useStyles = makeStyles(({ palette, constants }: ECTheme) => { 12 | return createStyles({ 13 | root: { 14 | border: `1px solid ${palette.additional["gray"][4]}`, 15 | borderRadius: "3px", 16 | padding: constants.generalUnit * 2, 17 | }, 18 | title: { 19 | marginBottom: constants.generalUnit * 4, 20 | }, 21 | }) 22 | }) 23 | 24 | const NodeReadyForFork = () => { 25 | const classes = useStyles() 26 | const theme: ECTheme = useTheme() 27 | 28 | const data = { 29 | labels: ["1", "2", "3", "4", "5", "6", "7"], 30 | datasets: [ 31 | { 32 | label: "Node count: eth1", 33 | data: [65, 59, 80, 81, 56, 55, 40], 34 | fill: false, 35 | borderColor: theme.palette.primary.main, 36 | lineTension: 0.3, 37 | }, 38 | ], 39 | } 40 | 41 | const options = { 42 | scales: { 43 | y: { 44 | display: false, 45 | }, 46 | x: { 47 | display: false, 48 | grid: { 49 | display: false, 50 | }, 51 | }, 52 | }, 53 | plugins: { 54 | legend: { 55 | display: false, 56 | }, 57 | }, 58 | } 59 | 60 | return ( 61 |
62 | 63 | node ready to fork 64 | 65 |
66 | 67 |
68 |
69 | ) 70 | } 71 | 72 | export default NodeReadyForFork 73 | -------------------------------------------------------------------------------- /src/Components/Modules/DemographicsStats/StatusSync.tsx: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2021 ChainSafe Systems 3 | SPDX-License-Identifier: LGPL-3.0-only 4 | */ 5 | import React from "react" 6 | import { createStyles, makeStyles, useTheme } from "@chainsafe/common-theme" 7 | import { Typography } from "@chainsafe/common-components" 8 | import { Scatter } from "react-chartjs-2" 9 | import { ECTheme } from "../../Themes/types" 10 | 11 | const useStyles = makeStyles(({ palette, constants }: ECTheme) => { 12 | return createStyles({ 13 | root: { 14 | border: `1px solid ${palette.additional["gray"][4]}`, 15 | borderRadius: "3px", 16 | padding: constants.generalUnit * 2, 17 | }, 18 | title: { 19 | marginBottom: constants.generalUnit * 4, 20 | }, 21 | }) 22 | }) 23 | 24 | const getRandomArr = (length: number) => { 25 | const arrXY: { x: number; y: number }[] = [] 26 | for (let i = 0; i < length; i++) { 27 | arrXY.push({ 28 | x: Math.floor(Math.random() * 100), 29 | y: Math.floor(Math.random() * 100), 30 | }) 31 | } 32 | return arrXY 33 | } 34 | 35 | const StatusSync = () => { 36 | const classes = useStyles() 37 | const theme: ECTheme = useTheme() 38 | 39 | const data = { 40 | datasets: [ 41 | { 42 | label: "Node sync", 43 | data: getRandomArr(50), 44 | backgroundColor: theme.palette.primary.main, 45 | }, 46 | ], 47 | } 48 | 49 | const options = { 50 | scales: { 51 | y: { 52 | display: false, 53 | }, 54 | x: { 55 | display: false, 56 | grid: { 57 | display: false, 58 | }, 59 | }, 60 | }, 61 | plugins: { 62 | legend: { 63 | display: false, 64 | }, 65 | }, 66 | } 67 | 68 | return ( 69 |
70 | 71 | Status sync over time 72 | 73 |
74 | 75 |
76 |
77 | ) 78 | } 79 | 80 | export default StatusSync 81 | -------------------------------------------------------------------------------- /src/Components/Modules/Footer.tsx: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2021 ChainSafe Systems 3 | SPDX-License-Identifier: LGPL-3.0-only 4 | */ 5 | 6 | import React from "react" 7 | import { createStyles, ITheme, makeStyles } from "@chainsafe/common-theme" 8 | import { Typography } from "@chainsafe/common-components" 9 | 10 | const useStyles = makeStyles(({ palette, constants, breakpoints }: ITheme) => { 11 | return createStyles({ 12 | root: { 13 | padding: `${constants.generalUnit * 2}px ${constants.generalUnit}px`, 14 | background: palette.background.paper, 15 | display: "flex", 16 | [breakpoints.down("sm")]: { 17 | flexDirection: "column", 18 | }, 19 | }, 20 | bold: { 21 | fontWeight: 600, 22 | }, 23 | copyright: { 24 | display: "flex", 25 | alignItems: "center", 26 | fontFamily: "Neue Montreal", 27 | color: palette.additional["gray"][4], 28 | marginRight: constants.generalUnit, 29 | [breakpoints.up("md")]: { 30 | marginLeft: constants.generalUnit * 2, 31 | }, 32 | [breakpoints.up("xl")]: { 33 | textAlign: "left", 34 | fontSize: constants.generalUnit * 2, 35 | }, 36 | }, 37 | link: { 38 | color: palette.additional["gray"][4], 39 | transition: "all .25s ease-out", 40 | marginLeft: "2px", 41 | "&:hover": { 42 | color: palette.primary.main, 43 | }, 44 | }, 45 | }) 46 | }) 47 | 48 | const Footer: React.FC = () => { 49 | const currentYear = new Date().getFullYear() 50 | const classes = useStyles() 51 | return ( 52 | 70 | ) 71 | } 72 | export default Footer 73 | -------------------------------------------------------------------------------- /src/Components/Modules/HeatMap/MapLeaflet.tsx: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2021 ChainSafe Systems 3 | SPDX-License-Identifier: LGPL-3.0-only 4 | */ 5 | import React, { useEffect } from "react" 6 | // import { CircleMarker, MapContainer, TileLayer, Tooltip } from "react-leaflet" 7 | import L, { LatLngTuple } from "leaflet" 8 | // import { createStyles, makeStyles } from "@chainsafe/common-theme" 9 | import { useEth2CrawlerApi } from "../../../Contexts/Eth2CrawlerContext" 10 | // import useWindowDimensions from "../../../utilHooks/useWindowDimensions" 11 | import "leaflet/dist/leaflet.css" 12 | import "leaflet.heat" 13 | import { useState } from "react" 14 | 15 | // const useStyles = makeStyles(() => { 16 | // return createStyles({ 17 | // mapContainer: { 18 | // height: "100%", 19 | // width: "100%", 20 | // }, 21 | // mapContainerDefined: { 22 | // height: "inherit", 23 | // width: "100%", 24 | // }, 25 | // }) 26 | // }) 27 | 28 | // attribution to put on map 29 | // const tilesAttribution = 30 | // "Map tiles by Stamen Design, under CC BY 3.0. Data by OpenStreetMap, under CC BY SA." 31 | 32 | // tiles source 33 | // const accessToken = "W4rWKMx2iiIF8SZAOjfFnuk4khsAjJJ2iwdTI8pKy4yN58BJHP02SiwIwVABnEmZ" 34 | // const tileUrl = `https://{s}.tile.jawg.io/jawg-dark/{z}/{x}/{y}{r}.png?access-token=${accessToken}` 35 | 36 | // const tileUrl = "https://{s}.basemaps.cartocdn.com/dark_nolabels/{z}/{x}/{y}{r}.png" 37 | 38 | const latLngCenter: LatLngTuple = [30, 0] 39 | // const latLngBounds: LatLngBoundsExpression = [ 40 | // [-70.095513, -140.0225067], 41 | // [86.120628, 170.31769], 42 | // ] 43 | const maxZoom = 5 44 | const defaultZoom = 1.4 45 | const minZoom = 1.4 46 | 47 | const NodeMap = ({ rootClassName }: { rootClassName: string }) => { 48 | // const classes = useStyles() 49 | const { heatmap } = useEth2CrawlerApi() 50 | // const { width } = useWindowDimensions() 51 | 52 | // const circleRadius = width < 480 ? 1 : width < 720 ? 2 : width < 1280 ? 4 : 4 53 | // const circleOpacity = 0.3 54 | 55 | const [map, setMap] = useState(undefined) 56 | const [radius, setRadius] = useState(3) 57 | 58 | useEffect(() => { 59 | if (!map) { 60 | const newMap = L.map("map", { 61 | zoomControl: true, 62 | minZoom: minZoom, 63 | maxZoom: maxZoom, 64 | // maxBounds: latLngBounds, 65 | attributionControl: false, 66 | }).setView(latLngCenter, defaultZoom) 67 | 68 | L.tileLayer("https://{s}.basemaps.cartocdn.com/dark_nolabels/{z}/{x}/{y}{r}.png").addTo( 69 | newMap 70 | ) 71 | newMap.on("zoomend", function(results: any) { 72 | if (results.target._zoom > 4) { 73 | setRadius(5) 74 | } else if (results.target._zoom > 3) { 75 | setRadius(4) 76 | } else { 77 | setRadius(3) 78 | } 79 | }); 80 | setMap(newMap) 81 | } 82 | }, [map]) 83 | 84 | useEffect(() => { 85 | if (map) { 86 | const points: any[] = heatmap 87 | ? heatmap.map((p) => { 88 | return [p.latitude, p.longitude] 89 | }) 90 | : [] 91 | 92 | // removing previous layer 93 | let heatlayer: any = undefined; 94 | map.eachLayer((layer: any) => { 95 | if(layer?.options?.radius) { 96 | heatlayer = layer 97 | } 98 | }) 99 | if (heatlayer) { 100 | map.removeLayer(heatlayer) 101 | } 102 | 103 | ;(L as any) 104 | .heatLayer(points, { 105 | minOpacity: 1, 106 | radius: radius, 107 | max: 1, 108 | blur: 5, 109 | }) 110 | .addTo(map) 111 | } 112 | }, [heatmap, map, radius]) 113 | 114 | return
115 | 116 | // pure react leaflet map 117 | // in case we need it later 118 | 119 | // return ( 120 | //
121 | // 132 | // 133 | // {heatmap.map((heatmapPoint, i) => ( 134 | // 141 | // 142 | // {`${heatmapPoint.clientType} : ${heatmapPoint.networkType}`} 143 | // 144 | // 145 | // ))} 146 | // 147 | //
148 | // ) 149 | } 150 | 151 | export default NodeMap 152 | -------------------------------------------------------------------------------- /src/Components/Modules/Navbar.tsx: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2021 ChainSafe Systems 3 | SPDX-License-Identifier: LGPL-3.0-only 4 | */ 5 | 6 | import React from "react" 7 | import { createStyles, ITheme, makeStyles } from "@chainsafe/common-theme" 8 | import { Typography } from "@chainsafe/common-components" 9 | 10 | const useStyles = makeStyles(({ breakpoints, palette, zIndex, constants }: ITheme) => { 11 | return createStyles({ 12 | container: { 13 | width: "100%", 14 | display: "flex", 15 | background: palette.background.default, 16 | position: "fixed", 17 | top: 0, 18 | zIndex: zIndex?.layer4, 19 | }, 20 | box: { 21 | padding: `${constants.generalUnit * 3}px ${constants.generalUnit * 4}px`, 22 | display: "flex", 23 | flex: 1, 24 | justifyContent: "space-between", 25 | [breakpoints.down("sm")]: { 26 | padding: `${constants.generalUnit * 3}px ${constants.generalUnit * 1}px`, 27 | }, 28 | }, 29 | navLink: { 30 | color: palette.common.white.main, 31 | textDecoration: "none", 32 | fontFamily: "Neue Montreal", 33 | fontWeight: "bold", 34 | "&:hover": { 35 | color: palette.primary.main, 36 | transition: "ease-in 0.2s", 37 | }, 38 | }, 39 | }) 40 | }) 41 | 42 | const NavBar: React.FC = () => { 43 | const classes = useStyles() 44 | return ( 45 |
46 |
47 | 48 | 49 | Eth2 Nodewatch 50 | 51 | 52 | 53 | 59 | GitHub 60 | 61 | 62 |
63 |
64 | ) 65 | } 66 | 67 | export default NavBar 68 | -------------------------------------------------------------------------------- /src/Components/Modules/NodeStats/NodeStatsOverTime.tsx: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2021 ChainSafe Systems 3 | SPDX-License-Identifier: LGPL-3.0-only 4 | */ 5 | import React, { useMemo } from "react" 6 | import { createStyles, makeStyles, useTheme } from "@chainsafe/common-theme" 7 | import { Typography } from "@chainsafe/common-components" 8 | import { useEth2CrawlerApi } from "../../../Contexts/Eth2CrawlerContext" 9 | import { ECTheme } from "../../Themes/types" 10 | import { 11 | XAxis, 12 | YAxis, 13 | ResponsiveContainer, 14 | LineChart, 15 | CartesianGrid, 16 | Line, 17 | Tooltip, 18 | } from "recharts" 19 | import ToolTipIcon from "../../Elements/Icons/ToolTipIcon" 20 | import ReactTooltip from "react-tooltip" 21 | import dayjs from "dayjs" 22 | 23 | const useStyles = makeStyles(({ constants, breakpoints, palette, typography }: ECTheme) => { 24 | return createStyles({ 25 | root: { 26 | padding: constants.generalUnit * 2, 27 | width: "inherit", 28 | height: "inherit", 29 | }, 30 | chartContainer: { 31 | height: "40vh", 32 | width: "100%", 33 | [breakpoints.down("sm")]: { 34 | height: "30vh", 35 | }, 36 | }, 37 | title: { 38 | color: palette.additional["gray"][2], 39 | }, 40 | headingContainer: { 41 | display: "flex", 42 | alignItems: "center", 43 | color: palette.additional["gray"][2], 44 | }, 45 | tooltipIcon: { 46 | width: 16, 47 | height: 16, 48 | marginLeft: constants.generalUnit, 49 | }, 50 | containerMargin: { 51 | marginBottom: constants.generalUnit * 4, 52 | }, 53 | tooltipBody: { 54 | ...typography.body1, 55 | backgroundColor: palette.additional["gray"][2], 56 | }, 57 | }) 58 | }) 59 | 60 | const NodeStatusOverTime = () => { 61 | const classes = useStyles() 62 | const theme: ECTheme = useTheme() 63 | 64 | const { nodeStatsOverTime } = useEth2CrawlerApi() 65 | 66 | const chartData = useMemo( 67 | () => 68 | nodeStatsOverTime.map( 69 | (nodeStat: { time: number; totalNodes: any; syncedNodes: any; unsyncedNodes: any }) => ({ 70 | time: dayjs(nodeStat.time * 1000).format("DD MMM 'YY"), 71 | total: nodeStat.totalNodes, 72 | synced: nodeStat.syncedNodes, 73 | unsynced: nodeStat.unsyncedNodes, 74 | }) 75 | ), 76 | [nodeStatsOverTime] 77 | ) 78 | 79 | return ( 80 |
81 |
82 |
83 | 84 | Node count over the past 7 days 85 | 86 | 87 | 88 | 89 | Shows node count over the past 7 days.
90 | The chart also shows number of nodes
synced and unsynced over the time period. 91 |
92 |
93 |
94 |
95 | 96 |
97 | 98 | 99 | 100 | 101 | 102 | 103 | 109 | 115 | 121 | 122 | 123 |
124 |
125 | ) 126 | } 127 | 128 | export default NodeStatusOverTime 129 | -------------------------------------------------------------------------------- /src/Components/Modules/SoftwareStats/AltAirPercentage.tsx: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2021 ChainSafe Systems 3 | SPDX-License-Identifier: LGPL-3.0-only 4 | */ 5 | import React, { useMemo, useState } from "react" 6 | import { createStyles, makeStyles, useTheme } from "@chainsafe/common-theme" 7 | import { ECTheme } from "../../Themes/types" 8 | import { PieChart, Pie, Sector, ResponsiveContainer } from "recharts" 9 | import { useEth2CrawlerApi } from "../../../Contexts/Eth2CrawlerContext" 10 | import { Typography } from "@chainsafe/common-components" 11 | 12 | const useStyles = makeStyles(({ constants, palette }: ECTheme) => { 13 | return createStyles({ 14 | root: { 15 | border: `1px solid ${palette.background.paper}`, 16 | borderRadius: "3px", 17 | padding: constants.generalUnit * 2, 18 | width: "inherit", 19 | height: "inherit", 20 | }, 21 | chartContainer: { 22 | height: `${constants.chartSizes.chartHeight}px`, 23 | }, 24 | title: { 25 | marginBottom: constants.generalUnit * 4, 26 | color: palette.text.primary, 27 | }, 28 | }) 29 | }) 30 | 31 | const renderActiveShape = (props: any, fill: string) => { 32 | const { cx, cy, innerRadius, outerRadius, startAngle, endAngle, payload } = props 33 | 34 | return ( 35 | 36 | 37 | {payload.name} 38 | 39 | 48 | 57 | 58 | ) 59 | } 60 | 61 | const AltAirPercentage: React.FC = () => { 62 | const classes = useStyles() 63 | const theme: ECTheme = useTheme() 64 | const [activeIndex, setActiveIndex] = useState(0) 65 | 66 | const onPieEnter = (_: any, index: number) => { 67 | setActiveIndex(index) 68 | } 69 | 70 | const { altAirPercentage } = useEth2CrawlerApi() 71 | 72 | const data = useMemo(() => { 73 | return altAirPercentage !== undefined 74 | ? [ 75 | { 76 | name: `Nodes ready (${altAirPercentage.toFixed(1)})%`, 77 | value: altAirPercentage, 78 | }, 79 | { 80 | name: `Nodes not ready (${(100 - altAirPercentage).toFixed(1)})%`, 81 | value: 100 - altAirPercentage, 82 | }, 83 | ] 84 | : [] 85 | }, [altAirPercentage]) 86 | 87 | return ( 88 |
89 | 90 | Nodes ready for Altair upgrade 91 | 92 |
93 | 94 | 95 | 98 | renderActiveShape(props, theme.constants.chartPrimaryColors.main) 99 | } 100 | data={data} 101 | cx="50%" 102 | cy="50%" 103 | innerRadius={100} 104 | outerRadius={120} 105 | fill={theme.constants.chartPrimaryColors.main} 106 | dataKey="value" 107 | onMouseEnter={onPieEnter} 108 | /> 109 | 110 | 111 |
112 |
113 | ) 114 | } 115 | 116 | export default AltAirPercentage 117 | -------------------------------------------------------------------------------- /src/Components/Modules/SoftwareStats/NetworkTypes.tsx: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2021 ChainSafe Systems 3 | SPDX-License-Identifier: LGPL-3.0-only 4 | */ 5 | import React, { useMemo } from "react" 6 | import { createStyles, makeStyles, useTheme } from "@chainsafe/common-theme" 7 | import { Typography } from "@chainsafe/common-components" 8 | import { useEth2CrawlerApi } from "../../../Contexts/Eth2CrawlerContext" 9 | import { ECTheme } from "../../Themes/types" 10 | import { BarChart, Bar, Tooltip, XAxis, YAxis, ResponsiveContainer } from "recharts" 11 | 12 | const useStyles = makeStyles(({ palette, constants }: ECTheme) => { 13 | return createStyles({ 14 | root: { 15 | border: `1px solid ${palette.background.paper}`, 16 | borderRadius: "3px", 17 | padding: constants.generalUnit * 2, 18 | width: "inherit", 19 | height: "inherit", 20 | }, 21 | chartContainer: { 22 | height: `${constants.chartSizes.chartHeight}px`, 23 | }, 24 | title: { 25 | marginBottom: constants.generalUnit * 2, 26 | color: palette.text.primary, 27 | }, 28 | }) 29 | }) 30 | 31 | // const MIN_NETWORK_TYPE_COUNT = 50 32 | 33 | const NetworkTypes = () => { 34 | const classes = useStyles() 35 | const theme: ECTheme = useTheme() 36 | 37 | const { networks } = useEth2CrawlerApi() 38 | 39 | const chartData = useMemo( 40 | () => 41 | networks 42 | .sort((first, second) => (first.count > second.count ? 1 : -1)) 43 | // .filter((network) => network.count > MIN_NETWORK_TYPE_COUNT) 44 | .map((network) => ({ 45 | name: network.name || "unknown", 46 | count: network.count, 47 | })), 48 | [networks] 49 | ) 50 | 51 | return ( 52 |
53 | 54 | Network types distribution 55 | 56 |
57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 |
66 |
67 | ) 68 | } 69 | 70 | export default NetworkTypes 71 | -------------------------------------------------------------------------------- /src/Components/Modules/SoftwareStats/OperatingSystems.tsx: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2021 ChainSafe Systems 3 | SPDX-License-Identifier: LGPL-3.0-only 4 | */ 5 | import React, { useMemo } from "react" 6 | import { createStyles, makeStyles, useTheme } from "@chainsafe/common-theme" 7 | import { Typography } from "@chainsafe/common-components" 8 | import { useEth2CrawlerApi } from "../../../Contexts/Eth2CrawlerContext" 9 | import { ECTheme } from "../../Themes/types" 10 | import { BarChart, Bar, Tooltip, XAxis, YAxis, ResponsiveContainer } from "recharts" 11 | 12 | const useStyles = makeStyles(({ palette, constants }: ECTheme) => { 13 | return createStyles({ 14 | root: { 15 | border: `1px solid ${palette.background.paper}`, 16 | borderRadius: "3px", 17 | padding: constants.generalUnit * 2, 18 | width: "inherit", 19 | height: "inherit", 20 | }, 21 | chartContainer: { 22 | height: `${constants.chartSizes.chartHeight}px`, 23 | }, 24 | title: { 25 | marginBottom: constants.generalUnit * 2, 26 | color: palette.text.primary, 27 | }, 28 | }) 29 | }) 30 | 31 | // const MIN_OPERATING_SYSTEM_COUNT = 5 32 | 33 | const OperatingSystems = () => { 34 | const classes = useStyles() 35 | const theme: ECTheme = useTheme() 36 | 37 | const { operatingSystems } = useEth2CrawlerApi() 38 | 39 | const chartData = useMemo( 40 | () => 41 | operatingSystems 42 | .sort((first, second) => (first.count > second.count ? 1 : -1)) 43 | // .filter((operatingSystem) => operatingSystem.count > MIN_OPERATING_SYSTEM_COUNT) 44 | .map((operatingSystem) => ({ 45 | name: operatingSystem.name || "unknown", 46 | count: operatingSystem.count, 47 | })), 48 | [operatingSystems] 49 | ) 50 | 51 | return ( 52 |
53 | 54 | Operating systems distribution 55 | 56 |
57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 |
66 |
67 | ) 68 | } 69 | 70 | export default OperatingSystems 71 | -------------------------------------------------------------------------------- /src/Components/Modules/SoftwareStats/PercentageOfNodes.tsx: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2021 ChainSafe Systems 3 | SPDX-License-Identifier: LGPL-3.0-only 4 | */ 5 | import React from "react" 6 | import { createStyles, makeStyles } from "@chainsafe/common-theme" 7 | import { ECTheme } from "../../Themes/types" 8 | import { Typography } from "@chainsafe/common-components" 9 | 10 | const useStyles = makeStyles(({ palette, constants }: ECTheme) => { 11 | return createStyles({ 12 | root: { 13 | border: `1px solid ${palette.background.paper}`, 14 | borderRadius: "3px", 15 | padding: constants.generalUnit * 2, 16 | }, 17 | title: { 18 | marginBottom: constants.generalUnit * 4, 19 | }, 20 | statTitle: { 21 | color: palette.text.primary, 22 | }, 23 | }) 24 | }) 25 | 26 | const PERCENT = 2.24 27 | 28 | const PercentageOfNodes = () => { 29 | const classes = useStyles() 30 | 31 | return ( 32 |
33 | 34 | Nodes out of sync for the past 5 days 35 | 36 |
37 | 38 | {PERCENT}% 39 | 40 | 41 | of nodes contain out of sync nodes 42 | 43 |
44 |
45 | ) 46 | } 47 | 48 | export default PercentageOfNodes 49 | -------------------------------------------------------------------------------- /src/Components/Modules/SoftwareStats/VersionVariance.tsx: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2021 ChainSafe Systems 3 | SPDX-License-Identifier: LGPL-3.0-only 4 | */ 5 | import React, { useMemo } from "react" 6 | import { createStyles, makeStyles, useTheme } from "@chainsafe/common-theme" 7 | import { ECTheme } from "../../Themes/types" 8 | import { Typography } from "@chainsafe/common-components" 9 | import { useEth2CrawlerApi } from "../../../Contexts/Eth2CrawlerContext" 10 | import { BarChart, Bar, Tooltip, XAxis, YAxis, ResponsiveContainer } from "recharts" 11 | import { useCallback } from "react" 12 | import ToolTipIcon from "../../Elements/Icons/ToolTipIcon" 13 | import ReactTooltip from "react-tooltip" 14 | 15 | const useStyles = makeStyles(({ palette, constants }: ECTheme) => { 16 | return createStyles({ 17 | root: { 18 | border: `1px solid ${palette.background.paper}`, 19 | borderRadius: "3px", 20 | padding: constants.generalUnit * 2, 21 | width: "inherit", 22 | }, 23 | chartContainer: { 24 | height: "280px", 25 | }, 26 | title: { 27 | color: palette.text.primary, 28 | }, 29 | charts: { 30 | display: "grid", 31 | gridTemplateColumns: "1fr 1fr", 32 | }, 33 | eachChart: { 34 | width: "25%", 35 | height: "100%", 36 | }, 37 | tooltipIcon: { 38 | width: 16, 39 | height: 16, 40 | marginLeft: constants.generalUnit, 41 | }, 42 | headingContainer: { 43 | display: "flex", 44 | alignItems: "center", 45 | color: palette.additional["gray"][2], 46 | marginBottom: constants.generalUnit * 2, 47 | }, 48 | }) 49 | }) 50 | 51 | const VersionVariance = () => { 52 | const classes = useStyles() 53 | const theme: ECTheme = useTheme() 54 | const backgroundColors = Object.values(theme.constants.chartColors) 55 | 56 | const { clientVersions } = useEth2CrawlerApi() 57 | 58 | const sortedClientVersions = useMemo( 59 | () => 60 | clientVersions 61 | .sort((a, b) => (a.count < b.count ? -1 : 1)) 62 | .map((clientVersion) => { 63 | const versions = clientVersion.versions.sort((a, b) => (a.count > b.count ? -1 : 1)) 64 | if (versions.length > 5) { 65 | const first4Versions = [] 66 | for (let i = 0; i < 4; i++) { 67 | first4Versions.push(versions[i]) 68 | } 69 | let othersCount = 0 70 | for (let i = 4; i < versions.length; i++) { 71 | othersCount += versions[i].count 72 | } 73 | return { 74 | ...clientVersion, 75 | versions: [...first4Versions, { name: "others", count: othersCount }], 76 | } 77 | } else { 78 | return { 79 | ...clientVersion, 80 | versions, 81 | } 82 | } 83 | }), 84 | [clientVersions] 85 | ) 86 | 87 | const chartData = useMemo( 88 | () => 89 | sortedClientVersions 90 | .map((clientVersion) => { 91 | const stack: Record = { 92 | name: clientVersion.client, 93 | } 94 | clientVersion.versions.forEach((version) => { 95 | stack[`${clientVersion.client} ${version.name}`] = version.count 96 | }) 97 | return stack 98 | }) 99 | .flat(), 100 | [sortedClientVersions] 101 | ) 102 | 103 | const getUniqueBars = useCallback(() => { 104 | const bars: any[] = [] 105 | sortedClientVersions.forEach((clientVersion) => { 106 | clientVersion.versions.forEach((version, j) => { 107 | if (!bars.find((bar) => bar.key === `${clientVersion.client} ${version.name}`)) { 108 | bars.push({ 109 | key: `${clientVersion.client} ${version.name}`, 110 | dataKey: `${clientVersion.client} ${version.name}`, 111 | stackId: clientVersion.client, 112 | fill: backgroundColors[j], 113 | count: version.count, 114 | }) 115 | } 116 | }) 117 | }) 118 | return bars 119 | }, [sortedClientVersions, backgroundColors]) 120 | 121 | const bars = getUniqueBars() 122 | 123 | return ( 124 |
125 |
126 | 127 | Version variance across clients 128 | 129 | 130 | 131 | 132 | Shows variations in version of node clients
133 | Shows top 5 versions of known clients 134 |
135 |
136 |
137 |
138 | 139 | 140 | 141 | 142 | 143 | {bars.map((bar) => ( 144 | 145 | ))} 146 | 147 | 148 |
149 |
150 | ) 151 | } 152 | 153 | export default VersionVariance 154 | -------------------------------------------------------------------------------- /src/Components/Pages/HomePage.tsx: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2021 ChainSafe Systems 3 | SPDX-License-Identifier: LGPL-3.0-only 4 | */ 5 | import React from "react" 6 | import { createStyles, makeStyles } from "@chainsafe/common-theme" 7 | import { ECTheme } from "../Themes/types" 8 | import ClientTypes from "../Modules/DemographicsStats/ClientTypes" 9 | import HeatMap from "../Modules/HeatMap/MapLeaflet" 10 | import NetworkTypes from "../Modules/SoftwareStats/NetworkTypes" 11 | import OperatingSystems from "../Modules/SoftwareStats/OperatingSystems" 12 | import { useEth2CrawlerApi } from "../../Contexts/Eth2CrawlerContext" 13 | import VersionVariance from "../Modules/SoftwareStats/VersionVariance" 14 | import SectionTile from "../Layouts/SectionTile/SectionTile" 15 | import CardStat from "../Layouts/SectionTile/CardStat" 16 | import NodeStatusOverTime from "../Modules/NodeStats/NodeStatsOverTime" 17 | import GridLayoutWrapper from "../Layouts/GridLayout/GridLayoutWrapper" 18 | import { Typography } from "@chainsafe/common-components" 19 | import CountryStats from "../Modules/CountryStats" 20 | import AltAirPercentage from "../Modules/SoftwareStats/AltAirPercentage" 21 | 22 | const useStyles = makeStyles(({ constants, breakpoints, palette }: ECTheme) => { 23 | return createStyles({ 24 | root: { 25 | margin: `${constants.generalUnit * 11}px 0 ${constants.generalUnit * 8}px`, 26 | [breakpoints.down("lg")]: { 27 | margin: `${constants.generalUnit * 11}px ${constants.generalUnit * 4}px`, 28 | }, 29 | [breakpoints.down("md")]: { 30 | margin: `${constants.generalUnit * 11}px ${constants.generalUnit * 1}px`, 31 | }, 32 | background: palette.background.default, 33 | }, 34 | title: { 35 | marginRight: constants.generalUnit, 36 | marginBottom: constants.generalUnit * 3, 37 | }, 38 | nodeDemographics: {}, 39 | nodeMapRoot: { 40 | height: "50vh", 41 | width: "100%", 42 | [breakpoints.down("lg")]: { 43 | height: "50vh", 44 | }, 45 | [breakpoints.down("md")]: { 46 | height: "45vh", 47 | }, 48 | [breakpoints.down("sm")]: { 49 | height: "40vh", 50 | }, 51 | }, 52 | nodeStats: { 53 | display: "grid", 54 | gridColumnGap: constants.generalUnit, 55 | gridRowGap: constants.generalUnit * 3, 56 | gridTemplateColumns: "repeat(2, minmax(0,1fr))", 57 | maxWidth: "100%", 58 | [breakpoints.down(1099)]: { 59 | gridTemplateColumns: "repeat(1, minmax(0,1fr))", 60 | }, 61 | marginBottom: constants.generalUnit * 4, 62 | }, 63 | container: { 64 | marginBottom: constants.generalUnit * 4, 65 | }, 66 | }) 67 | }) 68 | 69 | function HomePage() { 70 | const classes = useStyles() 71 | const { nodeStats, nodeRegionalStats } = useEth2CrawlerApi() 72 | 73 | return ( 74 |
75 | 79 | 80 | 86 | If the head of a node is within 256 epochs or 1 day
87 | of the head of the chain, we consider it synced. 88 | 89 | } 90 | tooltipId="syncedPercentage" 91 | /> 92 | 98 | If the head of a node is behind the head of the chain
99 | by 256 epochs or 1 day, we consider it unsynced. 100 | 101 | } 102 | tooltipId="unsyncedPercentage" 103 | /> 104 | 105 | } 106 | > 107 | 108 |
109 | 113 | 117 | 126 | If a node is running on a cloud service, 127 |
we consider it hosted. 128 | 129 | } 130 | tooltipId="hostedPercentage" 131 | /> 132 | 141 | If a node is running on a network other than a cloud service,
such as 142 | residential or business, we consider it non-hosted. 143 | 144 | } 145 | tooltipId="nonHostedPercentage" 146 | /> 147 | 148 | } 149 | > 150 |
151 | 152 |
153 |
154 | 155 | 156 |
157 | 158 | 159 | 160 | 161 | 162 |
163 |
164 |
165 | ) 166 | } 167 | 168 | export default HomePage 169 | -------------------------------------------------------------------------------- /src/Components/Themes/constants.ts: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2021 ChainSafe Systems 3 | SPDX-License-Identifier: LGPL-3.0-only 4 | */ 5 | import { IConstants } from "@chainsafe/common-theme" 6 | 7 | export interface EcConstants extends IConstants { 8 | chartPrimaryColors: { 9 | main: string 10 | light: string 11 | dark: string 12 | } 13 | chartColors: { 14 | color1: string 15 | color2: string 16 | color3: string 17 | color4: string 18 | color5: string 19 | } 20 | statColors: { 21 | red: string 22 | blue: string 23 | green: string 24 | } 25 | chartSizes: { 26 | chartBoxHeight: number 27 | chartHeight: number 28 | } 29 | headerHeight: number 30 | } 31 | -------------------------------------------------------------------------------- /src/Components/Themes/theme.ts: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2021 ChainSafe Systems 3 | SPDX-License-Identifier: LGPL-3.0-only 4 | */ 5 | import { createTheme } from "@chainsafe/common-theme" 6 | import { EcConstants } from "./constants" 7 | 8 | export const theme = createTheme({ 9 | themeConfig: { 10 | palette: { 11 | primary: { 12 | main: "#B7C1FC", 13 | hover: "#F9B189", 14 | background: "#131825", 15 | }, 16 | secondary: { 17 | main: "#E4665C", 18 | }, 19 | background: { 20 | default: "#131825", 21 | paper: "#424F60", 22 | }, 23 | text: { 24 | primary: "#f6f6f6", 25 | }, 26 | }, 27 | constants: { 28 | ...({ 29 | chartPrimaryColors: { 30 | main: "#566BDF", 31 | light: "2E00B0", 32 | dark: "#000000", 33 | }, 34 | chartColors: { 35 | color1: "#566BDF", 36 | color2: "#10B981", 37 | color3: "#E4665C", 38 | color4: "#FFA113", 39 | color5: "#8C14EB", 40 | }, 41 | statColors: { 42 | red: "#E4665C", 43 | blue: "#B7C1FC", 44 | green: "#10B981", 45 | }, 46 | chartSizes: { 47 | chartBoxHeight: 300, 48 | chartHeight: 280, 49 | }, 50 | headerHeight: 50, 51 | } as EcConstants), 52 | }, 53 | overrides: { 54 | Typography: { 55 | h1: { 56 | fontSize: 48, 57 | lineHeight: "58px", 58 | fontStyle: "normal", 59 | fontWeight: "normal", 60 | }, 61 | h2: { 62 | fontSize: "38.0413px", 63 | lineHeight: "45px", 64 | fontStyle: "normal", 65 | fontWeight: "normal", 66 | }, 67 | h3: { 68 | fontSize: "22.3773px", 69 | lineHeight: "31px", 70 | fontStyle: "normal", 71 | fontWeight: "normal", 72 | }, 73 | h4: { 74 | fontSize: 20, 75 | lineHeight: "28px", 76 | fontStyle: "normal", 77 | fontWeight: "normal", 78 | }, 79 | }, 80 | }, 81 | }, 82 | }) 83 | -------------------------------------------------------------------------------- /src/Components/Themes/types.ts: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2021 ChainSafe Systems 3 | SPDX-License-Identifier: LGPL-3.0-only 4 | */ 5 | import { ITheme } from "@chainsafe/common-theme" 6 | import { EcConstants } from "./constants" 7 | 8 | export type ECTheme = ITheme 9 | -------------------------------------------------------------------------------- /src/Contexts/Eth2CrawlerContext.tsx: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2021 ChainSafe Systems 3 | SPDX-License-Identifier: LGPL-3.0-only 4 | */ 5 | import React, { useState, useEffect } from "react" 6 | import { GraphQLClient } from "graphql-request" 7 | import { 8 | GetClientCounts, 9 | GetClientCounts_aggregateByAgentName, 10 | } from "../GraphQL/types/GetClientCounts" 11 | import { 12 | GetOperatingSystems, 13 | GetOperatingSystems_aggregateByOperatingSystem, 14 | } from "../GraphQL/types/GetOperatingSystems" 15 | import { GetNetworks, GetNetworks_aggregateByNetwork } from "../GraphQL/types/GetNetworks" 16 | import { GetHeatmap, GetHeatmap_getHeatmapData } from "../GraphQL/types/GetHeatmap" 17 | import { 18 | GetClientVersions, 19 | GetClientVersions_aggregateByClientVersion, 20 | } from "../GraphQL/types/GetClientVersions" 21 | import { 22 | LOAD_CLIENTS, 23 | LOAD_NETWORKS, 24 | LOAD_OPERATING_SYSTEMS, 25 | LOAD_HEATMAP, 26 | LOAD_CLIENT_VERSIONS, 27 | LOAD_NODE_COUNTS, 28 | LOAD_NODE_COUNT_OVER_TIME, 29 | LOAD_REGIONAL_STATS, 30 | LOAD_NODES_BY_COUNTRIES, 31 | LOAD_ALTAIR_UPGRADE_PERCENTAGE, 32 | } from "../GraphQL/Queries" 33 | import { GetNodeStats, GetNodeStats_getNodeStats } from "../GraphQL/types/GetNodeStats" 34 | import { 35 | GetNodeStatsOverTime, 36 | GetNodeStatsOverTime_getNodeStatsOverTime, 37 | } from "../GraphQL/types/GetNodeStatsOverTime" 38 | import { getUnixTimeStampCurrent, getUnixTimeStampFromDaysBefore } from "../utils/dateUtils" 39 | import { 40 | GetRegionalStats, 41 | GetRegionalStats_getRegionalStats, 42 | } from "../GraphQL/types/getRegionalStats" 43 | import { 44 | GetNodesByCountries, 45 | GetNodesByCountries_aggregateByCountry, 46 | } from "../GraphQL/types/GetNodesByCountries" 47 | import { GetAltAirUpgradePercentage } from "../GraphQL/types/GetAltAirUpgradePercentage" 48 | 49 | type Eth2CrawlerContextProps = { 50 | children: React.ReactNode | React.ReactNode[] 51 | } 52 | 53 | interface IEth2CrawlerContext { 54 | clients: GetClientCounts_aggregateByAgentName[] 55 | operatingSystems: GetOperatingSystems_aggregateByOperatingSystem[] 56 | networks: GetNetworks_aggregateByNetwork[] 57 | clientVersions: GetClientVersions_aggregateByClientVersion[] 58 | heatmap: GetHeatmap_getHeatmapData[] 59 | nodeStats: GetNodeStats_getNodeStats | undefined 60 | nodeStatsOverTime: GetNodeStatsOverTime_getNodeStatsOverTime[] 61 | nodeRegionalStats: GetRegionalStats_getRegionalStats | undefined 62 | nodeCountByCountries: GetNodesByCountries_aggregateByCountry[] 63 | altAirPercentage: number | undefined 64 | isLoadingClients: boolean 65 | isLoadingOperatingSystems: boolean 66 | isLoadingNetworks: boolean 67 | isLoadingHeatmap: boolean 68 | isLoadingClientVersions: boolean 69 | isLoadingNodeStats: boolean 70 | isLoadingNodeStatsOverTime: boolean 71 | isLoadingNodeRegionalStats: boolean 72 | isLoadingNodeCountByCountries: boolean 73 | isLoadingAltAirPercentage: boolean 74 | } 75 | 76 | const Eth2CrawlerContext = React.createContext(undefined) 77 | 78 | const subgraphUrl = process.env.REACT_APP_GRAPHQL_URL || "" 79 | const graphClient = new GraphQLClient(subgraphUrl) 80 | 81 | const Eth2CrawlerProvider = ({ children }: Eth2CrawlerContextProps) => { 82 | const [nodeStats, setNodeStats] = useState(undefined) 83 | const [nodeRegionalStats, setNodeRegionalStats] = useState< 84 | GetRegionalStats_getRegionalStats | undefined 85 | >(undefined) 86 | const [nodeStatsOverTime, setNodeStatsOverTime] = useState< 87 | GetNodeStatsOverTime_getNodeStatsOverTime[] 88 | >([]) 89 | const [clients, setClients] = useState([]) 90 | const [operatingSystems, setOperatingSystems] = useState< 91 | GetOperatingSystems_aggregateByOperatingSystem[] 92 | >([]) 93 | const [networks, setNetworks] = useState([]) 94 | const [clientVersions, setClientVersions] = useState< 95 | GetClientVersions_aggregateByClientVersion[] 96 | >([]) 97 | const [heatmap, setHeatmap] = useState([]) 98 | const [nodeCountByCountries, setNodeCountByCountries] = useState< 99 | GetNodesByCountries_aggregateByCountry[] 100 | >([]) 101 | const [altAirPercentage, setAltAirPercentage] = useState(undefined) 102 | 103 | const [isLoadingClients, setIsLoadingClients] = useState(true) 104 | const [isLoadingOperatingSystems, setIsLoadingOperatingSystems] = useState(true) 105 | const [isLoadingNetworks, setIsLoadingNetworks] = useState(true) 106 | const [isLoadingHeatmap, setIsLoadingHeatmap] = useState(true) 107 | const [isLoadingClientVersions, setIsLoadingClientVersions] = useState(true) 108 | const [isLoadingNodeStats, setIsLoadingNodeStats] = useState(true) 109 | const [isLoadingNodeStatsOverTime, setIsLoadingNodeStatsOverTime] = useState(true) 110 | const [isLoadingNodeRegionalStats, setIsLoadingNodeRegionalStats] = useState(true) 111 | const [isLoadingNodeCountByCountries, setIsLoadingNodeCountByCountries] = useState(true) 112 | const [isLoadingAltAirPercentage, setIsLoadingAltAirPercentage] = useState(true) 113 | 114 | const getInitialData = async () => { 115 | graphClient 116 | .request(LOAD_NODE_COUNTS, { 117 | percentage: 15, 118 | }) 119 | .then((result) => { 120 | setNodeStats(result.getNodeStats) 121 | }) 122 | .catch(console.error) 123 | .finally(() => setIsLoadingNodeStats(false)) 124 | graphClient 125 | .request(LOAD_NODE_COUNT_OVER_TIME, { 126 | start: getUnixTimeStampFromDaysBefore(7), 127 | end: getUnixTimeStampCurrent(), 128 | }) 129 | .then((result) => { 130 | setNodeStatsOverTime(result.getNodeStatsOverTime) 131 | }) 132 | .catch(console.error) 133 | .finally(() => setIsLoadingNodeStatsOverTime(false)) 134 | graphClient 135 | .request(LOAD_REGIONAL_STATS) 136 | .then((result) => { 137 | setNodeRegionalStats(result.getRegionalStats) 138 | }) 139 | .catch(console.error) 140 | .finally(() => setIsLoadingNodeRegionalStats(false)) 141 | graphClient 142 | .request(LOAD_CLIENTS) 143 | .then((result) => { 144 | setClients(result.aggregateByAgentName) 145 | }) 146 | .catch(console.error) 147 | .finally(() => setIsLoadingClients(false)) 148 | graphClient 149 | .request(LOAD_OPERATING_SYSTEMS) 150 | .then((result) => { 151 | setOperatingSystems(result.aggregateByOperatingSystem) 152 | }) 153 | .catch(console.error) 154 | .finally(() => setIsLoadingOperatingSystems(false)) 155 | graphClient 156 | .request(LOAD_NETWORKS) 157 | .then((result) => { 158 | setNetworks(result.aggregateByNetwork) 159 | }) 160 | .catch(console.error) 161 | .finally(() => setIsLoadingNetworks(false)) 162 | graphClient 163 | .request(LOAD_HEATMAP) 164 | .then((result) => { 165 | setHeatmap(result.getHeatmapData) 166 | }) 167 | .catch(console.error) 168 | .finally(() => setIsLoadingHeatmap(false)) 169 | graphClient 170 | .request(LOAD_CLIENT_VERSIONS) 171 | .then((result) => { 172 | setClientVersions(result.aggregateByClientVersion) 173 | }) 174 | .catch(console.error) 175 | .finally(() => setIsLoadingClientVersions(false)) 176 | graphClient 177 | .request(LOAD_NODES_BY_COUNTRIES) 178 | .then((result) => { 179 | setNodeCountByCountries(result.aggregateByCountry) 180 | }) 181 | .catch(console.error) 182 | .finally(() => setIsLoadingNodeCountByCountries(false)) 183 | graphClient 184 | .request(LOAD_ALTAIR_UPGRADE_PERCENTAGE) 185 | .then((result) => { 186 | setAltAirPercentage(result.getAltairUpgradePercentage) 187 | }) 188 | .catch(console.error) 189 | .finally(() => setIsLoadingAltAirPercentage(false)) 190 | } 191 | 192 | useEffect(() => { 193 | getInitialData() 194 | }, []) 195 | 196 | return ( 197 | 221 | {children} 222 | 223 | ) 224 | } 225 | 226 | const useEth2CrawlerApi = () => { 227 | const context = React.useContext(Eth2CrawlerContext) 228 | if (context === undefined) { 229 | throw new Error("useEth2CrawlerApi must be used within a Eth2CrawlerProvider") 230 | } 231 | return context 232 | } 233 | 234 | export { Eth2CrawlerProvider, useEth2CrawlerApi } 235 | -------------------------------------------------------------------------------- /src/GraphQL/Queries.ts: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2021 ChainSafe Systems 3 | SPDX-License-Identifier: LGPL-3.0-only 4 | */ 5 | import { gql } from "apollo-boost" 6 | 7 | export const LOAD_NODE_COUNTS = gql` 8 | query GetNodeStats { 9 | getNodeStats { 10 | totalNodes 11 | nodeSyncedPercentage 12 | nodeUnsyncedPercentage 13 | } 14 | } 15 | ` 16 | 17 | export const LOAD_NODE_COUNT_OVER_TIME = gql` 18 | query GetNodeStatsOverTime($start: Float!, $end: Float!) { 19 | getNodeStatsOverTime(start: $start, end: $end) { 20 | time 21 | totalNodes 22 | syncedNodes 23 | unsyncedNodes 24 | } 25 | } 26 | ` 27 | 28 | export const LOAD_REGIONAL_STATS = gql` 29 | query GetRegionalStats { 30 | getRegionalStats { 31 | totalParticipatingCountries 32 | hostedNodePercentage 33 | nonhostedNodePercentage 34 | } 35 | } 36 | ` 37 | 38 | export const LOAD_CLIENTS = gql` 39 | query GetClientCounts { 40 | aggregateByAgentName { 41 | name 42 | count 43 | } 44 | } 45 | ` 46 | 47 | export const LOAD_OPERATING_SYSTEMS = gql` 48 | query GetOperatingSystems { 49 | aggregateByOperatingSystem { 50 | name 51 | count 52 | } 53 | } 54 | ` 55 | 56 | export const LOAD_NETWORKS = gql` 57 | query GetNetworks { 58 | aggregateByNetwork { 59 | name 60 | count 61 | } 62 | } 63 | ` 64 | 65 | export const LOAD_HEATMAP = gql` 66 | query GetHeatmap { 67 | getHeatmapData { 68 | networkType 69 | clientType 70 | syncStatus 71 | latitude 72 | longitude 73 | } 74 | } 75 | ` 76 | 77 | export const LOAD_CLIENT_VERSIONS = gql` 78 | query GetClientVersions { 79 | aggregateByClientVersion { 80 | client 81 | count 82 | versions { 83 | name 84 | count 85 | } 86 | } 87 | } 88 | ` 89 | 90 | export const LOAD_NODES_BY_COUNTRIES = gql` 91 | query GetNodesByCountries { 92 | aggregateByCountry { 93 | name 94 | count 95 | } 96 | } 97 | ` 98 | 99 | export const LOAD_ALTAIR_UPGRADE_PERCENTAGE = gql` 100 | query GetAltAirUpgradePercentage { 101 | getAltairUpgradePercentage 102 | } 103 | ` 104 | -------------------------------------------------------------------------------- /src/GraphQL/types/GetAltAirUpgradePercentage.ts: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2021 ChainSafe Systems 3 | SPDX-License-Identifier: LGPL-3.0-only 4 | */ 5 | /* tslint:disable */ 6 | /* eslint-disable */ 7 | // @generated 8 | // This file was automatically generated and should not be edited. 9 | 10 | // ==================================================== 11 | // GraphQL query operation: GetAltAirUpgradePercentage 12 | // ==================================================== 13 | 14 | export interface GetAltAirUpgradePercentage { 15 | getAltairUpgradePercentage: number; 16 | } 17 | -------------------------------------------------------------------------------- /src/GraphQL/types/GetClientCounts.ts: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2021 ChainSafe Systems 3 | SPDX-License-Identifier: LGPL-3.0-only 4 | */ 5 | /* tslint:disable */ 6 | /* eslint-disable */ 7 | // @generated 8 | // This file was automatically generated and should not be edited. 9 | 10 | // ==================================================== 11 | // GraphQL query operation: GetClientCounts 12 | // ==================================================== 13 | 14 | export interface GetClientCounts_aggregateByAgentName { 15 | __typename: "AggregateData"; 16 | name: string; 17 | count: number; 18 | } 19 | 20 | export interface GetClientCounts { 21 | aggregateByAgentName: GetClientCounts_aggregateByAgentName[]; 22 | } 23 | -------------------------------------------------------------------------------- /src/GraphQL/types/GetClientVersions.ts: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2021 ChainSafe Systems 3 | SPDX-License-Identifier: LGPL-3.0-only 4 | */ 5 | /* tslint:disable */ 6 | /* eslint-disable */ 7 | // @generated 8 | // This file was automatically generated and should not be edited. 9 | 10 | // ==================================================== 11 | // GraphQL query operation: GetClientVersions 12 | // ==================================================== 13 | 14 | export interface GetClientVersions_aggregateByClientVersion_versions { 15 | __typename: "AggregateData"; 16 | name: string; 17 | count: number; 18 | } 19 | 20 | export interface GetClientVersions_aggregateByClientVersion { 21 | __typename: "ClientVersionAggregation"; 22 | client: string; 23 | count: number; 24 | versions: GetClientVersions_aggregateByClientVersion_versions[]; 25 | } 26 | 27 | export interface GetClientVersions { 28 | aggregateByClientVersion: GetClientVersions_aggregateByClientVersion[]; 29 | } 30 | -------------------------------------------------------------------------------- /src/GraphQL/types/GetHeatmap.ts: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2021 ChainSafe Systems 3 | SPDX-License-Identifier: LGPL-3.0-only 4 | */ 5 | /* tslint:disable */ 6 | /* eslint-disable */ 7 | // @generated 8 | // This file was automatically generated and should not be edited. 9 | 10 | // ==================================================== 11 | // GraphQL query operation: GetHeatmap 12 | // ==================================================== 13 | 14 | export interface GetHeatmap_getHeatmapData { 15 | __typename: "HeatmapData"; 16 | networkType: string; 17 | clientType: string; 18 | syncStatus: string; 19 | latitude: number; 20 | longitude: number; 21 | } 22 | 23 | export interface GetHeatmap { 24 | getHeatmapData: GetHeatmap_getHeatmapData[]; 25 | } 26 | -------------------------------------------------------------------------------- /src/GraphQL/types/GetNetworks.ts: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2021 ChainSafe Systems 3 | SPDX-License-Identifier: LGPL-3.0-only 4 | */ 5 | /* tslint:disable */ 6 | /* eslint-disable */ 7 | // @generated 8 | // This file was automatically generated and should not be edited. 9 | 10 | // ==================================================== 11 | // GraphQL query operation: GetNetworks 12 | // ==================================================== 13 | 14 | export interface GetNetworks_aggregateByNetwork { 15 | __typename: "AggregateData"; 16 | name: string; 17 | count: number; 18 | } 19 | 20 | export interface GetNetworks { 21 | aggregateByNetwork: GetNetworks_aggregateByNetwork[]; 22 | } 23 | -------------------------------------------------------------------------------- /src/GraphQL/types/GetNodeStats.ts: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2021 ChainSafe Systems 3 | SPDX-License-Identifier: LGPL-3.0-only 4 | */ 5 | /* tslint:disable */ 6 | /* eslint-disable */ 7 | // @generated 8 | // This file was automatically generated and should not be edited. 9 | 10 | // ==================================================== 11 | // GraphQL query operation: GetNodeStats 12 | // ==================================================== 13 | 14 | export interface GetNodeStats_getNodeStats { 15 | __typename: "NodeStats"; 16 | totalNodes: number; 17 | nodeSyncedPercentage: number; 18 | nodeUnsyncedPercentage: number; 19 | } 20 | 21 | export interface GetNodeStats { 22 | getNodeStats: GetNodeStats_getNodeStats; 23 | } 24 | -------------------------------------------------------------------------------- /src/GraphQL/types/GetNodeStatsOverTime.ts: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2021 ChainSafe Systems 3 | SPDX-License-Identifier: LGPL-3.0-only 4 | */ 5 | /* tslint:disable */ 6 | /* eslint-disable */ 7 | // @generated 8 | // This file was automatically generated and should not be edited. 9 | 10 | // ==================================================== 11 | // GraphQL query operation: GetNodeStatsOverTime 12 | // ==================================================== 13 | 14 | export interface GetNodeStatsOverTime_getNodeStatsOverTime { 15 | __typename: "NodeStatsOverTime"; 16 | time: number; 17 | totalNodes: number; 18 | syncedNodes: number; 19 | unsyncedNodes: number; 20 | } 21 | 22 | export interface GetNodeStatsOverTime { 23 | getNodeStatsOverTime: GetNodeStatsOverTime_getNodeStatsOverTime[]; 24 | } 25 | 26 | export interface GetNodeStatsOverTimeVariables { 27 | start: number; 28 | end: number; 29 | } 30 | -------------------------------------------------------------------------------- /src/GraphQL/types/GetNodesByCountries.ts: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2021 ChainSafe Systems 3 | SPDX-License-Identifier: LGPL-3.0-only 4 | */ 5 | /* tslint:disable */ 6 | /* eslint-disable */ 7 | // @generated 8 | // This file was automatically generated and should not be edited. 9 | 10 | // ==================================================== 11 | // GraphQL query operation: GetNodesByCountries 12 | // ==================================================== 13 | 14 | export interface GetNodesByCountries_aggregateByCountry { 15 | __typename: "AggregateData"; 16 | name: string; 17 | count: number; 18 | } 19 | 20 | export interface GetNodesByCountries { 21 | aggregateByCountry: GetNodesByCountries_aggregateByCountry[]; 22 | } 23 | -------------------------------------------------------------------------------- /src/GraphQL/types/GetOperatingSystems.ts: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2021 ChainSafe Systems 3 | SPDX-License-Identifier: LGPL-3.0-only 4 | */ 5 | /* tslint:disable */ 6 | /* eslint-disable */ 7 | // @generated 8 | // This file was automatically generated and should not be edited. 9 | 10 | // ==================================================== 11 | // GraphQL query operation: GetOperatingSystems 12 | // ==================================================== 13 | 14 | export interface GetOperatingSystems_aggregateByOperatingSystem { 15 | __typename: "AggregateData"; 16 | name: string; 17 | count: number; 18 | } 19 | 20 | export interface GetOperatingSystems { 21 | aggregateByOperatingSystem: GetOperatingSystems_aggregateByOperatingSystem[]; 22 | } 23 | -------------------------------------------------------------------------------- /src/GraphQL/types/getRegionalStats.ts: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2021 ChainSafe Systems 3 | SPDX-License-Identifier: LGPL-3.0-only 4 | */ 5 | /* tslint:disable */ 6 | /* eslint-disable */ 7 | // @generated 8 | // This file was automatically generated and should not be edited. 9 | 10 | // ==================================================== 11 | // GraphQL query operation: GetRegionalStats 12 | // ==================================================== 13 | 14 | export interface GetRegionalStats_getRegionalStats { 15 | __typename: "RegionalStats"; 16 | totalParticipatingCountries: number; 17 | hostedNodePercentage: number; 18 | nonhostedNodePercentage: number; 19 | } 20 | 21 | export interface GetRegionalStats { 22 | getRegionalStats: GetRegionalStats_getRegionalStats; 23 | } 24 | -------------------------------------------------------------------------------- /src/assets/fonts/Neue-montreal/NeueMontreal-Bold.otf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ChainSafe/nodewatch-ui/423a1184c5961462f98497c87aed21934fb5f03e/src/assets/fonts/Neue-montreal/NeueMontreal-Bold.otf -------------------------------------------------------------------------------- /src/assets/fonts/Neue-montreal/NeueMontreal-BoldItalic.otf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ChainSafe/nodewatch-ui/423a1184c5961462f98497c87aed21934fb5f03e/src/assets/fonts/Neue-montreal/NeueMontreal-BoldItalic.otf -------------------------------------------------------------------------------- /src/assets/fonts/Neue-montreal/NeueMontreal-Italic.otf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ChainSafe/nodewatch-ui/423a1184c5961462f98497c87aed21934fb5f03e/src/assets/fonts/Neue-montreal/NeueMontreal-Italic.otf -------------------------------------------------------------------------------- /src/assets/fonts/Neue-montreal/NeueMontreal-Light.otf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ChainSafe/nodewatch-ui/423a1184c5961462f98497c87aed21934fb5f03e/src/assets/fonts/Neue-montreal/NeueMontreal-Light.otf -------------------------------------------------------------------------------- /src/assets/fonts/Neue-montreal/NeueMontreal-LightItalic.otf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ChainSafe/nodewatch-ui/423a1184c5961462f98497c87aed21934fb5f03e/src/assets/fonts/Neue-montreal/NeueMontreal-LightItalic.otf -------------------------------------------------------------------------------- /src/assets/fonts/Neue-montreal/NeueMontreal-Medium.otf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ChainSafe/nodewatch-ui/423a1184c5961462f98497c87aed21934fb5f03e/src/assets/fonts/Neue-montreal/NeueMontreal-Medium.otf -------------------------------------------------------------------------------- /src/assets/fonts/Neue-montreal/NeueMontreal-MediumItalic.otf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ChainSafe/nodewatch-ui/423a1184c5961462f98497c87aed21934fb5f03e/src/assets/fonts/Neue-montreal/NeueMontreal-MediumItalic.otf -------------------------------------------------------------------------------- /src/assets/fonts/Neue-montreal/NeueMontreal-Regular.otf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ChainSafe/nodewatch-ui/423a1184c5961462f98497c87aed21934fb5f03e/src/assets/fonts/Neue-montreal/NeueMontreal-Regular.otf -------------------------------------------------------------------------------- /src/dummyData/demographicsData.ts: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2021 ChainSafe Systems 3 | SPDX-License-Identifier: LGPL-3.0-only 4 | */ 5 | import { ClientType } from "../types/main" 6 | 7 | export const clients: { 8 | client: ClientType 9 | total: number 10 | }[] = [ 11 | { 12 | client: "geth", 13 | total: 23, 14 | }, 15 | { 16 | client: "parity", 17 | total: 80, 18 | }, 19 | { 20 | client: "getc", 21 | total: 30, 22 | }, 23 | { 24 | client: "ethereumjs", 25 | total: 40, 26 | }, 27 | { 28 | client: "multigeth", 29 | total: 50, 30 | }, 31 | ] 32 | -------------------------------------------------------------------------------- /src/dummyData/mapData.ts: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2021 ChainSafe Systems 3 | SPDX-License-Identifier: LGPL-3.0-only 4 | */ 5 | import { NodeInfo } from "../types/main" 6 | 7 | export const nodeLocations: NodeInfo[] = [ 8 | { 9 | name: "california", 10 | weight: 100, 11 | client: "geth", 12 | coordinates: [37.1930977, -123.7969029], 13 | }, 14 | { 15 | name: "tokyo", 16 | weight: 30, 17 | client: "getc", 18 | coordinates: [35.5090627, 139.2093774], 19 | }, 20 | { 21 | name: "johannesburg", 22 | weight: 50, 23 | client: "ethereumjs", 24 | coordinates: [-26.1713505, 27.9699847], 25 | }, 26 | { 27 | name: "madrid", 28 | weight: 40, 29 | client: "multigeth", 30 | coordinates: [40.4381311, -3.8196201], 31 | }, 32 | { 33 | name: "dhaka", 34 | weight: 70, 35 | client: "parity", 36 | coordinates: [23.7807777, 90.3492856], 37 | }, 38 | ] 39 | -------------------------------------------------------------------------------- /src/index.css: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2021 ChainSafe Systems 3 | SPDX-License-Identifier: LGPL-3.0-only 4 | */ 5 | @font-face { 6 | font-family: "Neue Montreal"; 7 | src: local("Neue Montreal"), 8 | url(./assets/fonts/Neue-montreal/NeueMontreal-Medium.otf) format("opentype"); 9 | } 10 | 11 | body { 12 | margin: 0; 13 | font-family: Neue Montreal; 14 | -webkit-font-smoothing: antialiased; 15 | -moz-osx-font-smoothing: grayscale; 16 | } 17 | 18 | code { 19 | font-family: source-code-pro, Menlo, Monaco, Consolas, "Courier New", monospace; 20 | } 21 | -------------------------------------------------------------------------------- /src/index.tsx: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2021 ChainSafe Systems 3 | SPDX-License-Identifier: LGPL-3.0-only 4 | */ 5 | import React from "react" 6 | import ReactDOM from "react-dom" 7 | import App from "./App" 8 | import reportWebVitals from "./reportWebVitals" 9 | import "./index.css" 10 | 11 | ReactDOM.render( 12 | 13 | 14 | , 15 | document.getElementById("root") 16 | ) 17 | 18 | // If you want to start measuring performance in your app, pass a function 19 | // to log results (for example: reportWebVitals(console.log)) 20 | // or send to an analytics endpoint. Learn more: https://bit.ly/CRA-vitals 21 | reportWebVitals() 22 | -------------------------------------------------------------------------------- /src/react-app-env.d.ts: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2021 ChainSafe Systems 3 | SPDX-License-Identifier: LGPL-3.0-only 4 | */ 5 | /// 6 | -------------------------------------------------------------------------------- /src/reportWebVitals.ts: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2021 ChainSafe Systems 3 | SPDX-License-Identifier: LGPL-3.0-only 4 | */ 5 | import { ReportHandler } from "web-vitals" 6 | 7 | const reportWebVitals = (onPerfEntry?: ReportHandler) => { 8 | if (onPerfEntry && onPerfEntry instanceof Function) { 9 | import("web-vitals").then(({ getCLS, getFID, getFCP, getLCP, getTTFB }) => { 10 | getCLS(onPerfEntry) 11 | getFID(onPerfEntry) 12 | getFCP(onPerfEntry) 13 | getLCP(onPerfEntry) 14 | getTTFB(onPerfEntry) 15 | }) 16 | } 17 | } 18 | 19 | export default reportWebVitals 20 | -------------------------------------------------------------------------------- /src/setupTests.ts: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2021 ChainSafe Systems 3 | SPDX-License-Identifier: LGPL-3.0-only 4 | */ 5 | // jest-dom adds custom jest matchers for asserting on DOM nodes. 6 | // allows you to do things like: 7 | // expect(element).toHaveTextContent(/react/i) 8 | // learn more: https://github.com/testing-library/jest-dom 9 | import "@testing-library/jest-dom" 10 | -------------------------------------------------------------------------------- /src/types/graphql-global-types.ts: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2021 ChainSafe Systems 3 | SPDX-License-Identifier: LGPL-3.0-only 4 | */ 5 | /* tslint:disable */ 6 | /* eslint-disable */ 7 | // @generated 8 | // This file was automatically generated and should not be edited. 9 | 10 | //============================================================== 11 | // START Enums and Input Objects 12 | //============================================================== 13 | 14 | //============================================================== 15 | // END Enums and Input Objects 16 | //============================================================== 17 | export {} 18 | -------------------------------------------------------------------------------- /src/types/index.d.ts: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2021 ChainSafe Systems 3 | SPDX-License-Identifier: LGPL-3.0-only 4 | */ 5 | declare module "leaflet.heat" 6 | -------------------------------------------------------------------------------- /src/types/main.ts: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2021 ChainSafe Systems 3 | SPDX-License-Identifier: LGPL-3.0-only 4 | */ 5 | export type ClientType = "geth" | "parity" | "ethereumjs" | "getc" | "nethermind" | "multigeth" 6 | 7 | export type NetworkType = "mainnet" | "classic" | "ropsten" | "rinkeby" | "kovan" | "goerli" 8 | 9 | export interface NodeInfo { 10 | name: string 11 | weight: number 12 | client: ClientType 13 | coordinates: [number, number] 14 | } 15 | -------------------------------------------------------------------------------- /src/utilHooks/useWindowDimensions.ts: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2021 ChainSafe Systems 3 | SPDX-License-Identifier: LGPL-3.0-only 4 | */ 5 | import { useState, useEffect } from "react" 6 | 7 | function getWindowDimensions() { 8 | const { innerWidth: width, innerHeight: height } = window 9 | return { 10 | width, 11 | height, 12 | } 13 | } 14 | 15 | export default function useWindowDimensions() { 16 | const [windowDimensions, setWindowDimensions] = useState(getWindowDimensions()) 17 | 18 | useEffect(() => { 19 | function handleResize() { 20 | setWindowDimensions(getWindowDimensions()) 21 | } 22 | 23 | window.addEventListener("resize", handleResize) 24 | return () => window.removeEventListener("resize", handleResize) 25 | }, []) 26 | 27 | return windowDimensions 28 | } 29 | -------------------------------------------------------------------------------- /src/utils/dateUtils.ts: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2021 ChainSafe Systems 3 | SPDX-License-Identifier: LGPL-3.0-only 4 | */ 5 | export const getUnixTimeStampFromDaysBefore = (noOfDays: number) => { 6 | const date = new Date() 7 | date.setDate(date.getDate() - noOfDays) 8 | return Math.round(date.getTime() / 1000) 9 | } 10 | 11 | export const getUnixTimeStampCurrent = () => { 12 | return Math.round(new Date().getTime() / 1000) 13 | } 14 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "es5", 4 | "lib": [ 5 | "dom", 6 | "dom.iterable", 7 | "esnext" 8 | ], 9 | "allowJs": true, 10 | "skipLibCheck": true, 11 | "esModuleInterop": true, 12 | "allowSyntheticDefaultImports": true, 13 | "strict": true, 14 | "forceConsistentCasingInFileNames": true, 15 | "noFallthroughCasesInSwitch": true, 16 | "module": "esnext", 17 | "moduleResolution": "node", 18 | "resolveJsonModule": true, 19 | "isolatedModules": true, 20 | "noEmit": true, 21 | "jsx": "react-jsx" 22 | }, 23 | "include": [ 24 | "src" 25 | ] 26 | } 27 | --------------------------------------------------------------------------------