Add cluster stats tab to explorer (#11325)

This commit is contained in:
Justin Starry 2020-08-01 22:05:58 +08:00 committed by GitHub
parent 9bcfc51df1
commit 54d36d2cfb
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
10 changed files with 651 additions and 30 deletions

View File

@ -2283,6 +2283,20 @@
"ieee754": "^1.1.4"
}
},
"kind-of": {
"version": "6.0.3",
"resolved": "https://registry.npmjs.org/kind-of/-/kind-of-6.0.3.tgz",
"integrity": "sha512-dcS1ul+9tmeD95T+x28/ehLgd9mENa3LsvDTtzm3vyBEO7RPptvAD+t44WVXaUjTBRcrpFeFlC8WCruUR456hw=="
},
"superstruct": {
"version": "0.8.4",
"resolved": "https://registry.npmjs.org/superstruct/-/superstruct-0.8.4.tgz",
"integrity": "sha512-48Ors8IVWZm/tMr8r0Si6+mJiB7mkD7jqvIzktjJ4+EnP5tBp0qOpiM1J8sCUorKx+TXWrfb3i1UcjdD1YK/wA==",
"requires": {
"kind-of": "^6.0.2",
"tiny-invariant": "^1.0.6"
}
},
"tweetnacl": {
"version": "1.0.3",
"resolved": "https://registry.npmjs.org/tweetnacl/-/tweetnacl-1.0.3.tgz",
@ -3012,6 +3026,11 @@
"@types/react-router": "*"
}
},
"@types/socket.io-client": {
"version": "1.4.33",
"resolved": "https://registry.npmjs.org/@types/socket.io-client/-/socket.io-client-1.4.33.tgz",
"integrity": "sha512-m4LnxkljsI9fMsjwpW5QhRpMixo2BeeLpFmg0AE+sS4H1pzAd/cs/ftTiL60FLZgfFa8PFRPx5KsHu8O0bADKQ=="
},
"@types/stack-utils": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/@types/stack-utils/-/stack-utils-1.0.1.tgz",
@ -3366,6 +3385,11 @@
}
}
},
"after": {
"version": "0.8.2",
"resolved": "https://registry.npmjs.org/after/-/after-0.8.2.tgz",
"integrity": "sha1-/ts5T58OAqqXaOcCvaI7UF+ufh8="
},
"aggregate-error": {
"version": "3.0.1",
"resolved": "https://registry.npmjs.org/aggregate-error/-/aggregate-error-3.0.1.tgz",
@ -3580,6 +3604,11 @@
"es-abstract": "^1.17.0-next.1"
}
},
"arraybuffer.slice": {
"version": "0.0.7",
"resolved": "https://registry.npmjs.org/arraybuffer.slice/-/arraybuffer.slice-0.0.7.tgz",
"integrity": "sha512-wGUIVQXuehL5TCqQun8OW81jGzAWycqzFF8lFp+GOM5BXLYj3bKNsYC4daB7n6XjCqxQA/qgTJ+8ANR3acjrog=="
},
"arrify": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/arrify/-/arrify-1.0.1.tgz",
@ -4349,6 +4378,11 @@
"resolved": "https://registry.npmjs.org/babylon/-/babylon-6.18.0.tgz",
"integrity": "sha512-q/UEjfGJ2Cm3oKV71DJz9d25TPnq5rhBVL2Q4fA5wcC3jcrdn7+SssEybFIxwAvvP+YCsCYNKughoF33GxgycQ=="
},
"backo2": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/backo2/-/backo2-1.0.2.tgz",
"integrity": "sha1-MasayLEpNjRj41s+u2n038+6eUc="
},
"balanced-match": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.0.tgz",
@ -4417,6 +4451,11 @@
"safe-buffer": "^5.0.1"
}
},
"base64-arraybuffer": {
"version": "0.1.5",
"resolved": "https://registry.npmjs.org/base64-arraybuffer/-/base64-arraybuffer-0.1.5.tgz",
"integrity": "sha1-c5JncZI7Whl0etZmqlzUv5xunOg="
},
"base64-js": {
"version": "1.3.1",
"resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.3.1.tgz",
@ -4435,6 +4474,14 @@
"tweetnacl": "^0.14.3"
}
},
"better-assert": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/better-assert/-/better-assert-1.0.2.tgz",
"integrity": "sha1-QIZrnhueC1W0gYlDEeaPr/rrxSI=",
"requires": {
"callsite": "1.0.0"
}
},
"big.js": {
"version": "5.2.2",
"resolved": "https://registry.npmjs.org/big.js/-/big.js-5.2.2.tgz",
@ -4454,6 +4501,11 @@
"file-uri-to-path": "1.0.0"
}
},
"blob": {
"version": "0.0.5",
"resolved": "https://registry.npmjs.org/blob/-/blob-0.0.5.tgz",
"integrity": "sha512-gaqbzQPqOoamawKg0LGVd7SzLgXS+JH61oWprSLH+P+abTczqJbhTR8CmJ2u9/bUYNmHTGJx/UEmn6doAvvuig=="
},
"block-stream": {
"version": "0.0.9",
"resolved": "https://registry.npmjs.org/block-stream/-/block-stream-0.0.9.tgz",
@ -4833,6 +4885,11 @@
"caller-callsite": "^2.0.0"
}
},
"callsite": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/callsite/-/callsite-1.0.0.tgz",
"integrity": "sha1-KAOY5dZkvXQDi28JBRU+borxvCA="
},
"callsites": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/callsites/-/callsites-2.0.0.tgz",
@ -5263,11 +5320,21 @@
"resolved": "https://registry.npmjs.org/commondir/-/commondir-1.0.1.tgz",
"integrity": "sha1-3dgA2gxmEnOTzKWVDqloo6rxJTs="
},
"component-bind": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/component-bind/-/component-bind-1.0.0.tgz",
"integrity": "sha1-AMYIq33Nk4l8AAllGx06jh5zu9E="
},
"component-emitter": {
"version": "1.3.0",
"resolved": "https://registry.npmjs.org/component-emitter/-/component-emitter-1.3.0.tgz",
"integrity": "sha512-Rd3se6QB+sO1TwqZjscQrurpEPIfO0/yYnSin6Q/rD3mOutHvUrCAhJub3r90uNb+SESBuE0QYoB90YdfatsRg=="
},
"component-inherit": {
"version": "0.0.3",
"resolved": "https://registry.npmjs.org/component-inherit/-/component-inherit-0.0.3.tgz",
"integrity": "sha1-ZF/ErfWLcrZJ1crmUTVhnbJv8UM="
},
"compose-function": {
"version": "3.0.3",
"resolved": "https://registry.npmjs.org/compose-function/-/compose-function-3.0.3.tgz",
@ -5466,6 +5533,11 @@
}
}
},
"countup.js": {
"version": "1.9.3",
"resolved": "https://registry.npmjs.org/countup.js/-/countup.js-1.9.3.tgz",
"integrity": "sha1-zj5QzXFgRB5HjwfaMYle3MDxyd0="
},
"create-ecdh": {
"version": "4.0.3",
"resolved": "https://registry.npmjs.org/create-ecdh/-/create-ecdh-4.0.3.tgz",
@ -6309,6 +6381,46 @@
"once": "^1.4.0"
}
},
"engine.io-client": {
"version": "3.4.3",
"resolved": "https://registry.npmjs.org/engine.io-client/-/engine.io-client-3.4.3.tgz",
"integrity": "sha512-0NGY+9hioejTEJCaSJZfWZLk4FPI9dN+1H1C4+wj2iuFba47UgZbJzfWs4aNFajnX/qAaYKbe2lLTfEEWzCmcw==",
"requires": {
"component-emitter": "~1.3.0",
"component-inherit": "0.0.3",
"debug": "~4.1.0",
"engine.io-parser": "~2.2.0",
"has-cors": "1.1.0",
"indexof": "0.0.1",
"parseqs": "0.0.5",
"parseuri": "0.0.5",
"ws": "~6.1.0",
"xmlhttprequest-ssl": "~1.5.4",
"yeast": "0.1.2"
},
"dependencies": {
"ws": {
"version": "6.1.4",
"resolved": "https://registry.npmjs.org/ws/-/ws-6.1.4.tgz",
"integrity": "sha512-eqZfL+NE/YQc1/ZynhojeV8q+H050oR8AZ2uIev7RU10svA9ZnJUddHcOUZTJLinZ9yEfdA2kSATS2qZK5fhJA==",
"requires": {
"async-limiter": "~1.0.0"
}
}
}
},
"engine.io-parser": {
"version": "2.2.0",
"resolved": "https://registry.npmjs.org/engine.io-parser/-/engine.io-parser-2.2.0.tgz",
"integrity": "sha512-6I3qD9iUxotsC5HEMuuGsKA0cXerGz+4uGcXQEkfBidgKf0amsjrrtwcbwK/nzpZBxclXlV7gGl9dgWvu4LF6w==",
"requires": {
"after": "0.8.2",
"arraybuffer.slice": "~0.0.7",
"base64-arraybuffer": "0.1.5",
"blob": "0.0.5",
"has-binary2": "~1.0.2"
}
},
"enhanced-resolve": {
"version": "4.1.1",
"resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-4.1.1.tgz",
@ -7827,6 +7939,26 @@
}
}
},
"has-binary2": {
"version": "1.0.3",
"resolved": "https://registry.npmjs.org/has-binary2/-/has-binary2-1.0.3.tgz",
"integrity": "sha512-G1LWKhDSvhGeAQ8mPVQlqNcOB2sJdwATtZKl2pDKKHfpf/rYj24lkinxf69blJbnsvtqqNU+L3SL50vzZhXOnw==",
"requires": {
"isarray": "2.0.1"
},
"dependencies": {
"isarray": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/isarray/-/isarray-2.0.1.tgz",
"integrity": "sha1-o32U7ZzaLVmGXJ92/llu4fM4dB4="
}
}
},
"has-cors": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/has-cors/-/has-cors-1.1.0.tgz",
"integrity": "sha1-XkdHk/fqmEPRu5nCPu9J/xJv/zk="
},
"has-flag": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz",
@ -8137,6 +8269,11 @@
"resolved": "https://registry.npmjs.org/https-browserify/-/https-browserify-1.0.0.tgz",
"integrity": "sha1-7AbBDgo0wPL68Zn3/X/Hj//QPHM="
},
"humanize-duration-ts": {
"version": "2.1.1",
"resolved": "https://registry.npmjs.org/humanize-duration-ts/-/humanize-duration-ts-2.1.1.tgz",
"integrity": "sha512-TibNF2/fkypjAfHdGpWL/dmWUS0G6Qi+3mKyiB6LDCowbMy+PtzbgPTnFMNTOVAJXDau01jYrJ3tFoz5AJSqhA=="
},
"iconv-lite": {
"version": "0.4.24",
"resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz",
@ -8235,6 +8372,11 @@
"resolved": "https://registry.npmjs.org/indexes-of/-/indexes-of-1.0.1.tgz",
"integrity": "sha1-8w9xbI4r00bHtn0985FVZqfAVgc="
},
"indexof": {
"version": "0.0.1",
"resolved": "https://registry.npmjs.org/indexof/-/indexof-0.0.1.tgz",
"integrity": "sha1-gtwzbSMrkGIXnQWrMpOmYFn9Q10="
},
"infer-owner": {
"version": "1.0.4",
"resolved": "https://registry.npmjs.org/infer-owner/-/infer-owner-1.0.4.tgz",
@ -10920,6 +11062,11 @@
"resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz",
"integrity": "sha1-IQmtx5ZYh8/AXLvUQsrIv7s2CGM="
},
"object-component": {
"version": "0.0.3",
"resolved": "https://registry.npmjs.org/object-component/-/object-component-0.0.3.tgz",
"integrity": "sha1-8MaapQ78lbhmwYb0AKM3acsvEpE="
},
"object-copy": {
"version": "0.1.0",
"resolved": "https://registry.npmjs.org/object-copy/-/object-copy-0.1.0.tgz",
@ -11348,6 +11495,22 @@
"resolved": "https://registry.npmjs.org/parse5/-/parse5-4.0.0.tgz",
"integrity": "sha512-VrZ7eOd3T1Fk4XWNXMgiGBK/z0MG48BWG2uQNU4I72fkQuKUTZpl+u9k+CxEG0twMVzSmXEEz12z5Fnw1jIQFA=="
},
"parseqs": {
"version": "0.0.5",
"resolved": "https://registry.npmjs.org/parseqs/-/parseqs-0.0.5.tgz",
"integrity": "sha1-1SCKNzjkZ2bikbouoXNoSSGouJ0=",
"requires": {
"better-assert": "~1.0.0"
}
},
"parseuri": {
"version": "0.0.5",
"resolved": "https://registry.npmjs.org/parseuri/-/parseuri-0.0.5.tgz",
"integrity": "sha1-gCBKUNTbt3m/3G6+J3jZDkvOMgo=",
"requires": {
"better-assert": "~1.0.0"
}
},
"parseurl": {
"version": "1.3.3",
"resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz",
@ -12786,6 +12949,16 @@
"semver": "^5.6.0"
}
},
"react-countup": {
"version": "4.3.3",
"resolved": "https://registry.npmjs.org/react-countup/-/react-countup-4.3.3.tgz",
"integrity": "sha512-pWnxpwdPNRyJFha/YKKbyc4RLAw8PzmULdgCziGIgw6vxhT1VdccrvQgj38HBSoM2qF/MoLmn4M2klvDWVIdaw==",
"requires": {
"countup.js": "^1.9.3",
"prop-types": "^15.7.2",
"warning": "^4.0.3"
}
},
"react-dev-utils": {
"version": "10.2.1",
"resolved": "https://registry.npmjs.org/react-dev-utils/-/react-dev-utils-10.2.1.tgz",
@ -14408,6 +14581,69 @@
"kind-of": "^3.2.0"
}
},
"socket.io-client": {
"version": "2.3.0",
"resolved": "https://registry.npmjs.org/socket.io-client/-/socket.io-client-2.3.0.tgz",
"integrity": "sha512-cEQQf24gET3rfhxZ2jJ5xzAOo/xhZwK+mOqtGRg5IowZsMgwvHwnf/mCRapAAkadhM26y+iydgwsXGObBB5ZdA==",
"requires": {
"backo2": "1.0.2",
"base64-arraybuffer": "0.1.5",
"component-bind": "1.0.0",
"component-emitter": "1.2.1",
"debug": "~4.1.0",
"engine.io-client": "~3.4.0",
"has-binary2": "~1.0.2",
"has-cors": "1.1.0",
"indexof": "0.0.1",
"object-component": "0.0.3",
"parseqs": "0.0.5",
"parseuri": "0.0.5",
"socket.io-parser": "~3.3.0",
"to-array": "0.1.4"
},
"dependencies": {
"component-emitter": {
"version": "1.2.1",
"resolved": "https://registry.npmjs.org/component-emitter/-/component-emitter-1.2.1.tgz",
"integrity": "sha1-E3kY1teCg/ffemt8WmPhQOaUJeY="
}
}
},
"socket.io-parser": {
"version": "3.3.0",
"resolved": "https://registry.npmjs.org/socket.io-parser/-/socket.io-parser-3.3.0.tgz",
"integrity": "sha512-hczmV6bDgdaEbVqhAeVMM/jfUfzuEZHsQg6eOmLgJht6G3mPKMxYm75w2+qhAQZ+4X+1+ATZ+QFKeOZD5riHng==",
"requires": {
"component-emitter": "1.2.1",
"debug": "~3.1.0",
"isarray": "2.0.1"
},
"dependencies": {
"component-emitter": {
"version": "1.2.1",
"resolved": "https://registry.npmjs.org/component-emitter/-/component-emitter-1.2.1.tgz",
"integrity": "sha1-E3kY1teCg/ffemt8WmPhQOaUJeY="
},
"debug": {
"version": "3.1.0",
"resolved": "https://registry.npmjs.org/debug/-/debug-3.1.0.tgz",
"integrity": "sha512-OX8XqP7/1a9cqkxYw2yXss15f26NKWBpDXQd0/uK/KPqdQhxbPa994hnzjcE2VqQpDslf55723cKPUOGSmMY3g==",
"requires": {
"ms": "2.0.0"
}
},
"isarray": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/isarray/-/isarray-2.0.1.tgz",
"integrity": "sha1-o32U7ZzaLVmGXJ92/llu4fM4dB4="
},
"ms": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz",
"integrity": "sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g="
}
}
},
"sockjs": {
"version": "0.3.19",
"resolved": "https://registry.npmjs.org/sockjs/-/sockjs-0.3.19.tgz",
@ -14962,20 +15198,9 @@
}
},
"superstruct": {
"version": "0.8.4",
"resolved": "https://registry.npmjs.org/superstruct/-/superstruct-0.8.4.tgz",
"integrity": "sha512-48Ors8IVWZm/tMr8r0Si6+mJiB7mkD7jqvIzktjJ4+EnP5tBp0qOpiM1J8sCUorKx+TXWrfb3i1UcjdD1YK/wA==",
"requires": {
"kind-of": "^6.0.2",
"tiny-invariant": "^1.0.6"
},
"dependencies": {
"kind-of": {
"version": "6.0.3",
"resolved": "https://registry.npmjs.org/kind-of/-/kind-of-6.0.3.tgz",
"integrity": "sha512-dcS1ul+9tmeD95T+x28/ehLgd9mENa3LsvDTtzm3vyBEO7RPptvAD+t44WVXaUjTBRcrpFeFlC8WCruUR456hw=="
}
}
"version": "0.10.12",
"resolved": "https://registry.npmjs.org/superstruct/-/superstruct-0.10.12.tgz",
"integrity": "sha512-FiNhfegyytDI0QxrrEoeGknFM28SnoHqCBpkWewUm8jRNj74NVxLpiiePvkOo41Ze/aKMSHa/twWjNF81mKaQQ=="
},
"supports-color": {
"version": "5.5.0",
@ -15394,6 +15619,11 @@
"resolved": "https://registry.npmjs.org/tmpl/-/tmpl-1.0.4.tgz",
"integrity": "sha1-I2QN17QtAEM5ERQIIOXPRA5SHdE="
},
"to-array": {
"version": "0.1.4",
"resolved": "https://registry.npmjs.org/to-array/-/to-array-0.1.4.tgz",
"integrity": "sha1-F+bBH3PdTz10zaek/zI46a2b+JA="
},
"to-arraybuffer": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/to-arraybuffer/-/to-arraybuffer-1.0.1.tgz",
@ -15831,6 +16061,14 @@
"makeerror": "1.0.x"
}
},
"warning": {
"version": "4.0.3",
"resolved": "https://registry.npmjs.org/warning/-/warning-4.0.3.tgz",
"integrity": "sha512-rpJyN222KWIvHJ/F53XSZv0Zl/accqHR8et1kpaMTD/fLCRxtV8iX8czMzY7sVZupTI3zcUTg8eycS2kNF9l6w==",
"requires": {
"loose-envify": "^1.0.0"
}
},
"wasm-dce": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/wasm-dce/-/wasm-dce-1.0.2.tgz",
@ -16776,6 +17014,11 @@
"resolved": "https://registry.npmjs.org/xmlchars/-/xmlchars-2.2.0.tgz",
"integrity": "sha512-JZnDKK8B0RCDw84FNdDAIpZK+JuJw+s7Lz8nksI7SIuU3UXJJslUthsi+uWBUYOwPFwW7W7PRLRfUKpxjtjFCw=="
},
"xmlhttprequest-ssl": {
"version": "1.5.5",
"resolved": "https://registry.npmjs.org/xmlhttprequest-ssl/-/xmlhttprequest-ssl-1.5.5.tgz",
"integrity": "sha1-wodrBhaKrcQOV9l+gRkayPQ5iz4="
},
"xregexp": {
"version": "4.3.0",
"resolved": "https://registry.npmjs.org/xregexp/-/xregexp-4.3.0.tgz",
@ -16924,6 +17167,11 @@
"integrity": "sha512-L28STB170nwWS63UjtlEOE3dldQApaJXZkOI1uMFfzf3rRuPegHaHesyee+YxQ+W6SvRDQV6UrdOdRiR153wJg=="
}
}
},
"yeast": {
"version": "0.1.2",
"resolved": "https://registry.npmjs.org/yeast/-/yeast-0.1.2.tgz",
"integrity": "sha1-AI4G2AlDIMNy28L47XagymyKxBk="
}
}
}

View File

@ -14,16 +14,21 @@
"@types/react": "^16.9.43",
"@types/react-dom": "^16.9.8",
"@types/react-router-dom": "^5.1.5",
"@types/socket.io-client": "^1.4.33",
"bootstrap": "^4.5.0",
"bs58": "^4.0.1",
"humanize-duration-ts": "^2.1.1",
"node-sass": "^4.14.1",
"prettier": "^2.0.5",
"react": "^16.13.1",
"react-app-rewired": "^2.1.6",
"react-countup": "^4.3.3",
"react-dom": "^16.13.1",
"react-router-dom": "^5.2.0",
"react-scripts": "3.4.1",
"socket.io-client": "^2.3.0",
"solana-sdk-wasm": "file:wasm/pkg",
"superstruct": "^0.10.12",
"typescript": "^3.9.7",
"wasm-loader": "^1.3.0"
},

View File

@ -12,6 +12,7 @@ import { ACCOUNT_ALIASES, ACCOUNT_ALIASES_PLURAL } from "./providers/accounts";
import TabbedPage from "components/TabbedPage";
import TopAccountsCard from "components/TopAccountsCard";
import SupplyCard from "components/SupplyCard";
import StatsCard from "components/StatsCard";
import { pickCluster } from "utils/url";
import Banner from "components/Banner";
@ -84,11 +85,11 @@ function App() {
<AccountsCard />
</TabbedPage>
</Route>
<Route
render={({ location }) => (
<Redirect to={{ ...location, pathname: "/transactions" }} />
)}
></Route>
<Route>
<TabbedPage tab="Stats">
<StatsCard />
</TabbedPage>
</Route>
</Switch>
</div>
</>

View File

@ -0,0 +1,131 @@
import React from "react";
import CountUp from "react-countup";
import TableCardBody from "./common/TableCardBody";
import {
useDashboardInfo,
usePerformanceInfo,
useRootSlot,
PERF_UPDATE_SEC,
useSetActive,
} from "providers/stats/solanaBeach";
import { slotsToHumanString } from "utils";
import { useCluster, Cluster } from "providers/cluster";
export default function StatsCard() {
return (
<div className="card">
<div className="card-header">
<div className="row align-items-center">
<div className="col">
<h4 className="card-header-title">Live Cluster Info</h4>
</div>
</div>
</div>
<StatsCardBody />
</div>
);
}
function StatsCardBody() {
const rootSlot = useRootSlot();
const dashboardInfo = useDashboardInfo();
const performanceInfo = usePerformanceInfo();
const txTrackerRef = React.useRef({ old: 0, new: 0 });
const txTracker = txTrackerRef.current;
const setSocketActive = useSetActive();
const { cluster } = useCluster();
React.useEffect(() => {
setSocketActive(true);
return () => setSocketActive(false);
}, [setSocketActive, cluster]);
const statsAvailable =
cluster === Cluster.MainnetBeta || cluster === Cluster.Testnet;
if (!statsAvailable) {
return (
<div className="card-body text-center">
<div className="text-muted">
Stats are not available for this cluster
</div>
</div>
);
}
if (performanceInfo) {
const { totalTransactionCount: txCount, avgTPS } = performanceInfo;
// Track last tx count to initialize count up
if (txCount !== txTracker.new) {
// If this is the first tx count value, estimate the previous one
// in order to have a starting point for our animation
txTracker.old = txTracker.new || txCount - PERF_UPDATE_SEC * avgTPS;
txTracker.new = txCount;
}
} else {
txTrackerRef.current = { old: 0, new: 0 };
}
if (rootSlot === undefined || !dashboardInfo || !performanceInfo) {
return (
<div className="card-body text-center">
<span className="spinner-grow spinner-grow-sm mr-2"></span>
Loading
</div>
);
}
const currentBlock = rootSlot.toLocaleString("en-US");
const { avgBlockTime_1min, epochInfo } = dashboardInfo;
const averageBlockTime = Math.round(1000 * avgBlockTime_1min) + "ms";
const { slotIndex, slotsInEpoch } = epochInfo;
const currentEpoch = epochInfo.epoch.toString();
const epochProgress = ((100 * slotIndex) / slotsInEpoch).toFixed(1) + "%";
const epochTimeRemaining = slotsToHumanString(slotsInEpoch - slotIndex);
const transactionCount = (
<CountUp
start={txTracker.old}
end={txTracker.new}
duration={PERF_UPDATE_SEC + 2}
delay={0}
useEasing={false}
preserveValue={true}
separator=","
/>
);
const averageTps = Math.round(performanceInfo.avgTPS);
return (
<TableCardBody>
<tr>
<td className="w-100">Block</td>
<td className="text-right text-monospace">{currentBlock}</td>
</tr>
<tr>
<td className="w-100">Block time</td>
<td className="text-right text-monospace">{averageBlockTime}</td>
</tr>
<tr>
<td className="w-100">Epoch</td>
<td className="text-right text-monospace">{currentEpoch} </td>
</tr>
<tr>
<td className="w-100">Epoch progress</td>
<td className="text-right text-monospace">{epochProgress} </td>
</tr>
<tr>
<td className="w-100">Epoch time remaining</td>
<td className="text-right text-monospace">{epochTimeRemaining} </td>
</tr>
<tr>
<td className="w-100">Transaction count</td>
<td className="text-right text-monospace">{transactionCount} </td>
</tr>
<tr>
<td className="w-100">Transactions per second</td>
<td className="text-right text-monospace">{averageTps} </td>
</tr>
</TableCardBody>
);
}

View File

@ -4,7 +4,7 @@ import { useClusterModal } from "providers/cluster";
import ClusterStatusButton from "components/ClusterStatusButton";
import { pickCluster } from "utils/url";
export type Tab = "Transactions" | "Accounts" | "Supply";
export type Tab = "Transactions" | "Accounts" | "Supply" | "Stats";
type Props = { children: React.ReactNode; tab: Tab };
export default function TabbedPage({ children, tab }: Props) {
@ -22,6 +22,9 @@ export default function TabbedPage({ children, tab }: Props) {
<div className="row align-items-center">
<div className="col">
<ul className="nav nav-tabs nav-overflow header-tabs">
<li className="nav-item">
<NavLink href="/" tab="Stats" current={tab} />
</li>
<li className="nav-item">
<NavLink
href="/transactions"

View File

@ -9,19 +9,22 @@ import { RichListProvider } from "./providers/richList";
import { SupplyProvider } from "./providers/supply";
import { TransactionsProvider } from "./providers/transactions";
import { AccountsProvider } from "./providers/accounts";
import { StatsProvider } from "providers/stats";
ReactDOM.render(
<Router>
<ClusterProvider>
<SupplyProvider>
<RichListProvider>
<AccountsProvider>
<TransactionsProvider>
<App />
</TransactionsProvider>
</AccountsProvider>
</RichListProvider>
</SupplyProvider>
<StatsProvider>
<SupplyProvider>
<RichListProvider>
<AccountsProvider>
<TransactionsProvider>
<App />
</TransactionsProvider>
</AccountsProvider>
</RichListProvider>
</SupplyProvider>
</StatsProvider>
</ClusterProvider>
</Router>,
document.getElementById("root")

View File

@ -0,0 +1,7 @@
import React from "react";
import { SolanaBeachProvider } from "./solanaBeach";
type Props = { children: React.ReactNode };
export function StatsProvider({ children }: Props) {
return <SolanaBeachProvider>{children}</SolanaBeachProvider>;
}

View File

@ -0,0 +1,187 @@
import React from "react";
import io from "socket.io-client";
import {
object,
number,
is,
StructType,
array,
nullable,
any,
} from "superstruct";
import { useCluster, Cluster } from "providers/cluster";
// TODO: use `partial` when it is fixed
// https://github.com/ianstormtaylor/superstruct/issues/405
const DashboardInfo = object({
activatedStake: number(),
avgBlockTime_1h: number(),
avgBlockTime_1min: number(),
circulatingSupply: number(),
dailyPriceChange: number(),
dailyVolume: number(),
delinquentStake: number(),
epochInfo: object({
absoluteEpochStartSlot: number(),
absoluteSlot: number(),
blockHeight: number(),
epoch: number(),
slotIndex: number(),
slotsInEpoch: number(),
}),
stakingYield: number(),
tokenPrice: number(),
totalDelegatedStake: number(),
totalSupply: number(),
});
// TODO: use `partial` when it is fixed
// https://github.com/ianstormtaylor/superstruct/issues/405
const RootInfo = object({
currentLeader: any(),
nextLeaders: any(),
root: number(),
servedSlots: any(),
});
export const PERF_UPDATE_SEC = 5;
// TODO: use `partial` when it is fixed
// https://github.com/ianstormtaylor/superstruct/issues/405
const PerformanceInfo = object({
avgTPS: number(),
perfHistory: object({
s: array(nullable(number())),
m: array(nullable(number())),
l: array(nullable(number())),
}),
totalTransactionCount: number(),
});
type SetActive = React.Dispatch<React.SetStateAction<boolean>>;
const SetActiveContext = React.createContext<
{ setActive: SetActive } | undefined
>(undefined);
type RootInfo = StructType<typeof RootInfo>;
type RootState = { slot: number | undefined };
const RootContext = React.createContext<RootState | undefined>(undefined);
type DashboardInfo = StructType<typeof DashboardInfo>;
type DashboardState = { info: DashboardInfo | undefined };
const DashboardContext = React.createContext<DashboardState | undefined>(
undefined
);
type PerformanceInfo = StructType<typeof PerformanceInfo>;
type PerformanceState = { info: PerformanceInfo | undefined };
const PerformanceContext = React.createContext<PerformanceState | undefined>(
undefined
);
const MAINNET_URL = "https://api.solanabeach.io:8443/mainnet";
const TESTNET_URL = "https://api.solanabeach.io:8443/tds";
type Props = { children: React.ReactNode };
export function SolanaBeachProvider({ children }: Props) {
const { cluster } = useCluster();
const [active, setActive] = React.useState(false);
const [root, setRoot] = React.useState<number>();
const [dashboardInfo, setDashboardInfo] = React.useState<DashboardInfo>();
const [performanceInfo, setPerformanceInfo] = React.useState<
PerformanceInfo
>();
React.useEffect(() => {
if (!active) return;
let socket: SocketIOClient.Socket;
if (cluster === Cluster.MainnetBeta) {
socket = io(MAINNET_URL);
} else if (cluster === Cluster.Testnet) {
socket = io(TESTNET_URL);
} else {
return;
}
socket.on("connect", () => {
socket.emit("request_dashboardInfo");
socket.emit("request_performanceInfo");
});
socket.on("error", (err: any) => {
console.error(err);
});
socket.on("dashboardInfo", (data: any) => {
if (is(data, DashboardInfo)) {
setDashboardInfo(data);
}
});
socket.on("performanceInfo", (data: any) => {
if (is(data, PerformanceInfo)) {
setPerformanceInfo(data);
}
});
socket.on("rootNotification", (data: any) => {
if (is(data, RootInfo)) {
setRoot(data.root);
}
});
return () => {
socket.disconnect();
};
}, [active, cluster]);
// Reset info whenever the cluster changes
React.useEffect(() => {
return () => {
setDashboardInfo(undefined);
setPerformanceInfo(undefined);
setRoot(undefined);
};
}, [cluster]);
return (
<SetActiveContext.Provider value={{ setActive }}>
<DashboardContext.Provider value={{ info: dashboardInfo }}>
<PerformanceContext.Provider value={{ info: performanceInfo }}>
<RootContext.Provider value={{ slot: root }}>
{children}
</RootContext.Provider>
</PerformanceContext.Provider>
</DashboardContext.Provider>
</SetActiveContext.Provider>
);
}
export function useSetActive() {
const context = React.useContext(SetActiveContext);
if (!context) {
throw new Error(`useSetActive must be used within a StatsProvider`);
}
return context.setActive;
}
export function useDashboardInfo() {
const context = React.useContext(DashboardContext);
if (!context) {
throw new Error(`useDashboardInfo must be used within a StatsProvider`);
}
return context.info;
}
export function usePerformanceInfo() {
const context = React.useContext(PerformanceContext);
if (!context) {
throw new Error(`usePerformanceInfo must be used within a StatsProvider`);
}
return context.info;
}
export function useRootSlot() {
const context = React.useContext(RootContext);
if (!context) {
throw new Error(`useRootSlot must be used within a StatsProvider`);
}
return context.slot;
}

View File

@ -16,7 +16,7 @@ $path-to-fonts: "../../fonts" !default;
$white: #ffffff;
$gray-100: #f9fdfc;
$gray-200: #f1f8f6;
$gray-300: #d9efe7;
$gray-300: #e5ebe9;
$gray-400: #c6e6de;
$gray-500: #abd5c6;
$gray-600: #86b8b6;
@ -42,6 +42,7 @@ $danger: #43b5c5;
$light: $gray-100;
$dark: $gray-900;
$card-border-color: $gray-300;
$text-info-muted: $info-muted;
$navbar-light-active-color: $primary;
$theme-colors: (

View File

@ -1,4 +1,14 @@
import { LAMPORTS_PER_SOL } from "@solana/web3.js";
import {
HumanizeDuration,
HumanizeDurationLanguage,
} from "humanize-duration-ts";
export const NUM_TICKS_PER_SECOND = 160;
export const DEFAULT_TICKS_PER_SLOT = 64;
export const NUM_SLOTS_PER_SECOND =
NUM_TICKS_PER_SECOND / DEFAULT_TICKS_PER_SLOT;
export const MS_PER_SLOT = 1000 / NUM_SLOTS_PER_SECOND;
export function assertUnreachable(x: never): never {
throw new Error("Unreachable!");
@ -13,3 +23,28 @@ export function lamportsToSolString(
"◎" + new Intl.NumberFormat("en-US", { maximumFractionDigits }).format(sol)
);
}
const HUMANIZER = new HumanizeDuration(new HumanizeDurationLanguage());
HUMANIZER.setOptions({
language: "short",
spacer: "",
delimiter: " ",
round: true,
units: ["d", "h", "m", "s"],
largest: 3,
});
HUMANIZER.addLanguage("short", {
y: () => "y",
mo: () => "mo",
w: () => "w",
d: () => "d",
h: () => "h",
m: () => "m",
s: () => "s",
ms: () => "ms",
decimal: ".",
});
export function slotsToHumanString(slots: number): string {
return HUMANIZER.humanize(slots * MS_PER_SLOT);
}