Add live transaction stats card with history chart (#11813)
This commit is contained in:
65
explorer/package-lock.json
generated
65
explorer/package-lock.json
generated
@@ -3181,6 +3181,19 @@
|
||||
"resolved": "https://registry.npmjs.org/@types/chai/-/chai-4.2.12.tgz",
|
||||
"integrity": "sha512-aN5IAC8QNtSUdQzxu7lGBgYAOuU1tmRU4c9dIq5OKGf/SBVjXo+ffM2wEjudAWbgpOhy60nLoAGH1xm8fpCKFQ=="
|
||||
},
|
||||
"@types/chart.js": {
|
||||
"version": "2.9.23",
|
||||
"resolved": "https://registry.npmjs.org/@types/chart.js/-/chart.js-2.9.23.tgz",
|
||||
"integrity": "sha512-4QQNE/b+digosu3mnj4E7aNQGKnlpzXa9JvQYPtexpO7v9gnDeqwc1DxF8vLJWLDCNoO6hH0EgO8K/7PtJl8wg==",
|
||||
"requires": {
|
||||
"moment": "^2.10.2"
|
||||
}
|
||||
},
|
||||
"@types/classnames": {
|
||||
"version": "2.2.10",
|
||||
"resolved": "https://registry.npmjs.org/@types/classnames/-/classnames-2.2.10.tgz",
|
||||
"integrity": "sha512-1UzDldn9GfYYEsWWnn/P4wkTlkZDH7lDb0wBMGbtIQc9zXEQq7FlKBdZUn6OBqD8sKZZ2RQO2mAjGpXiDGoRmQ=="
|
||||
},
|
||||
"@types/color-name": {
|
||||
"version": "1.1.1",
|
||||
"resolved": "https://registry.npmjs.org/@types/color-name/-/color-name-1.1.1.tgz",
|
||||
@@ -5447,6 +5460,32 @@
|
||||
"resolved": "https://registry.npmjs.org/chardet/-/chardet-0.7.0.tgz",
|
||||
"integrity": "sha512-mT8iDcrh03qDGRRmoA2hmBJnxpllMR+0/0qlzjqZES6NdiWDcZkCNAk4rPFZ9Q85r27unkiNNg8ZOiwZXBHwcA=="
|
||||
},
|
||||
"chart.js": {
|
||||
"version": "2.9.3",
|
||||
"resolved": "https://registry.npmjs.org/chart.js/-/chart.js-2.9.3.tgz",
|
||||
"integrity": "sha512-+2jlOobSk52c1VU6fzkh3UwqHMdSlgH1xFv9FKMqHiNCpXsGPQa/+81AFa+i3jZ253Mq9aAycPwDjnn1XbRNNw==",
|
||||
"requires": {
|
||||
"chartjs-color": "^2.1.0",
|
||||
"moment": "^2.10.2"
|
||||
}
|
||||
},
|
||||
"chartjs-color": {
|
||||
"version": "2.4.1",
|
||||
"resolved": "https://registry.npmjs.org/chartjs-color/-/chartjs-color-2.4.1.tgz",
|
||||
"integrity": "sha512-haqOg1+Yebys/Ts/9bLo/BqUcONQOdr/hoEr2LLTRl6C5LXctUdHxsCYfvQVg5JIxITrfCNUDr4ntqmQk9+/0w==",
|
||||
"requires": {
|
||||
"chartjs-color-string": "^0.6.0",
|
||||
"color-convert": "^1.9.3"
|
||||
}
|
||||
},
|
||||
"chartjs-color-string": {
|
||||
"version": "0.6.0",
|
||||
"resolved": "https://registry.npmjs.org/chartjs-color-string/-/chartjs-color-string-0.6.0.tgz",
|
||||
"integrity": "sha512-TIB5OKn1hPJvO7JcteW4WY/63v6KwEdt6udfnDE9iCAZgy+V4SrbSxoIbTw/xkUIapjEI4ExGtD0+6D3KyFd7A==",
|
||||
"requires": {
|
||||
"color-name": "^1.0.0"
|
||||
}
|
||||
},
|
||||
"check-error": {
|
||||
"version": "1.0.2",
|
||||
"resolved": "https://registry.npmjs.org/check-error/-/check-error-1.0.2.tgz",
|
||||
@@ -5629,6 +5668,11 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"classnames": {
|
||||
"version": "2.2.6",
|
||||
"resolved": "https://registry.npmjs.org/classnames/-/classnames-2.2.6.tgz",
|
||||
"integrity": "sha512-JR/iSQOSt+LQIWwrwEzJ9uk0xfN3mTVYMwt1Ir5mUcSN6pU+V4zQFFaJsclJbPuAUQH+yfWef6tm7l1quW3C8Q=="
|
||||
},
|
||||
"clean-css": {
|
||||
"version": "4.2.3",
|
||||
"resolved": "https://registry.npmjs.org/clean-css/-/clean-css-4.2.3.tgz",
|
||||
@@ -10886,6 +10930,11 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"moment": {
|
||||
"version": "2.27.0",
|
||||
"resolved": "https://registry.npmjs.org/moment/-/moment-2.27.0.tgz",
|
||||
"integrity": "sha512-al0MUK7cpIcglMv3YF13qSgdAIqxHTO7brRtaz3DlSULbqfazqkc5kEjNrLDOM7fsjshoFIihnU8snrP7zUvhQ=="
|
||||
},
|
||||
"move-concurrently": {
|
||||
"version": "1.0.1",
|
||||
"resolved": "https://registry.npmjs.org/move-concurrently/-/move-concurrently-1.0.1.tgz",
|
||||
@@ -13180,6 +13229,22 @@
|
||||
"semver": "^5.6.0"
|
||||
}
|
||||
},
|
||||
"react-chartjs-2": {
|
||||
"version": "2.10.0",
|
||||
"resolved": "https://registry.npmjs.org/react-chartjs-2/-/react-chartjs-2-2.10.0.tgz",
|
||||
"integrity": "sha512-1MjWEkUn8LLFf6GVyYUOrruJTW3yVU5hlEJOwGj3MiokuC+jH/BahjWVGAMonbe9UYbEIUbd2Rn36iVlC0Hb7w==",
|
||||
"requires": {
|
||||
"lodash": "^4.17.19",
|
||||
"prop-types": "^15.7.2"
|
||||
},
|
||||
"dependencies": {
|
||||
"lodash": {
|
||||
"version": "4.17.20",
|
||||
"resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.20.tgz",
|
||||
"integrity": "sha512-PlhdFcillOINfeV7Ni6oF1TAEayyZBoZ8bcshTHqOYJYlrqzRK5hagpagky5o4HfCzzd1TRkXPMFq6cKk9rGmA=="
|
||||
}
|
||||
}
|
||||
},
|
||||
"react-countup": {
|
||||
"version": "4.3.3",
|
||||
"resolved": "https://registry.npmjs.org/react-countup/-/react-countup-4.3.3.tgz",
|
||||
|
@@ -12,6 +12,8 @@
|
||||
"@types/bn.js": "^4.11.6",
|
||||
"@types/bs58": "^4.0.1",
|
||||
"@types/chai": "^4.2.12",
|
||||
"@types/chart.js": "^2.9.23",
|
||||
"@types/classnames": "^2.2.10",
|
||||
"@types/jest": "^26.0.10",
|
||||
"@types/node": "^14.6.0",
|
||||
"@types/react": "^16.9.46",
|
||||
@@ -23,11 +25,14 @@
|
||||
"bootstrap": "^4.5.2",
|
||||
"bs58": "^4.0.1",
|
||||
"chai": "^4.2.0",
|
||||
"chart.js": "^2.9.3",
|
||||
"classnames": "2.2.6",
|
||||
"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-chartjs-2": "^2.10.0",
|
||||
"react-countup": "^4.3.3",
|
||||
"react-dom": "^16.13.1",
|
||||
"react-router-dom": "^5.2.0",
|
||||
|
271
explorer/src/components/TpsCard.tsx
Normal file
271
explorer/src/components/TpsCard.tsx
Normal file
@@ -0,0 +1,271 @@
|
||||
import React from "react";
|
||||
import { Bar } from "react-chartjs-2";
|
||||
import CountUp from "react-countup";
|
||||
import {
|
||||
usePerformanceInfo,
|
||||
PERF_UPDATE_SEC,
|
||||
PerformanceInfo,
|
||||
} from "providers/stats/solanaBeach";
|
||||
import classNames from "classnames";
|
||||
import { TableCardBody } from "components/common/TableCardBody";
|
||||
import { useCluster, Cluster } from "providers/cluster";
|
||||
import { ChartOptions, ChartTooltipModel } from "chart.js";
|
||||
|
||||
export function TpsCard() {
|
||||
return (
|
||||
<div className="card">
|
||||
<div className="card-header">
|
||||
<h4 className="card-header-title">Live Transaction Stats</h4>
|
||||
</div>
|
||||
<TpsCardBody />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function TpsCardBody() {
|
||||
const performanceInfo = usePerformanceInfo();
|
||||
const { cluster } = useCluster();
|
||||
|
||||
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) {
|
||||
return (
|
||||
<div className="card-body text-center">
|
||||
<span className="spinner-grow spinner-grow-sm mr-2"></span>
|
||||
Loading
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return <TpsBarChart performanceInfo={performanceInfo} />;
|
||||
}
|
||||
|
||||
type Series = "short" | "medium" | "long";
|
||||
const SERIES: Series[] = ["short", "medium", "long"];
|
||||
const SERIES_INFO = {
|
||||
short: {
|
||||
label: (index: number) => Math.floor(index / 4),
|
||||
interval: "30m",
|
||||
},
|
||||
medium: {
|
||||
label: (index: number) => index,
|
||||
interval: "2h",
|
||||
},
|
||||
long: {
|
||||
label: (index: number) => 3 * index,
|
||||
interval: "6h",
|
||||
},
|
||||
};
|
||||
|
||||
const CUSTOM_TOOLTIP = function (this: any, tooltipModel: ChartTooltipModel) {
|
||||
// Tooltip Element
|
||||
let tooltipEl = document.getElementById("chartjs-tooltip");
|
||||
|
||||
// Create element on first render
|
||||
if (!tooltipEl) {
|
||||
tooltipEl = document.createElement("div");
|
||||
tooltipEl.id = "chartjs-tooltip";
|
||||
tooltipEl.innerHTML = `<div class="content"></div>`;
|
||||
document.body.appendChild(tooltipEl);
|
||||
}
|
||||
|
||||
// Hide if no tooltip
|
||||
if (tooltipModel.opacity === 0) {
|
||||
tooltipEl.style.opacity = "0";
|
||||
return;
|
||||
}
|
||||
|
||||
// Set Text
|
||||
if (tooltipModel.body) {
|
||||
const { label, value } = tooltipModel.dataPoints[0];
|
||||
const tooltipContent = tooltipEl.querySelector("div");
|
||||
if (tooltipContent) {
|
||||
let innerHtml = `<div class="value">${value} TPS</div>`;
|
||||
innerHtml += `<div class="label">${label}</div>`;
|
||||
tooltipContent.innerHTML = innerHtml;
|
||||
}
|
||||
}
|
||||
|
||||
// Enable tooltip and set position
|
||||
const canvas: Element = this._chart.canvas;
|
||||
const position = canvas.getBoundingClientRect();
|
||||
tooltipEl.style.opacity = "1";
|
||||
tooltipEl.style.left =
|
||||
position.left + window.pageXOffset + tooltipModel.caretX + "px";
|
||||
tooltipEl.style.top =
|
||||
position.top + window.pageYOffset + tooltipModel.caretY + "px";
|
||||
};
|
||||
|
||||
const CHART_OPTIONS = (historyMaxTps: number): ChartOptions => {
|
||||
return {
|
||||
tooltips: {
|
||||
intersect: false, // Show tooltip when cursor in between bars
|
||||
enabled: false, // Hide default tooltip
|
||||
custom: CUSTOM_TOOLTIP,
|
||||
},
|
||||
legend: {
|
||||
display: false,
|
||||
},
|
||||
scales: {
|
||||
xAxes: [
|
||||
{
|
||||
ticks: {
|
||||
display: false,
|
||||
},
|
||||
gridLines: {
|
||||
display: false,
|
||||
},
|
||||
},
|
||||
],
|
||||
yAxes: [
|
||||
{
|
||||
ticks: {
|
||||
stepSize: 100,
|
||||
fontSize: 10,
|
||||
fontColor: "#EEE",
|
||||
beginAtZero: true,
|
||||
display: true,
|
||||
suggestedMax: historyMaxTps,
|
||||
},
|
||||
gridLines: {
|
||||
display: false,
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
animation: {
|
||||
duration: 0, // general animation time
|
||||
},
|
||||
hover: {
|
||||
animationDuration: 0, // duration of animations when hovering an item
|
||||
},
|
||||
responsiveAnimationDuration: 0, // animation duration after a resize
|
||||
};
|
||||
};
|
||||
|
||||
type TpsBarChartProps = { performanceInfo: PerformanceInfo };
|
||||
function TpsBarChart({ performanceInfo }: TpsBarChartProps) {
|
||||
const { perfHistory, avgTps, historyMaxTps } = performanceInfo;
|
||||
const [series, setSeries] = React.useState<Series>("short");
|
||||
const averageTps = Math.round(avgTps).toLocaleString("en-US");
|
||||
const transactionCount = <AnimatedTransactionCount info={performanceInfo} />;
|
||||
const seriesData = perfHistory[series];
|
||||
const chartOptions = React.useMemo(() => CHART_OPTIONS(historyMaxTps), [
|
||||
historyMaxTps,
|
||||
]);
|
||||
|
||||
const seriesLength = seriesData.length;
|
||||
const chartData: Chart.ChartData = {
|
||||
labels: seriesData.map((val, i) => {
|
||||
return `${SERIES_INFO[series].label(seriesLength - i)}min ago`;
|
||||
}),
|
||||
datasets: [
|
||||
{
|
||||
backgroundColor: "#00D192",
|
||||
hoverBackgroundColor: "#00D192",
|
||||
borderWidth: 0,
|
||||
data: seriesData.map((val) => val || 0),
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<TableCardBody>
|
||||
<tr>
|
||||
<td className="w-100">Transaction count</td>
|
||||
<td className="text-lg-right text-monospace">{transactionCount} </td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td className="w-100">Transactions per second (TPS)</td>
|
||||
<td className="text-lg-right text-monospace">{averageTps} </td>
|
||||
</tr>
|
||||
</TableCardBody>
|
||||
|
||||
<hr className="my-0" />
|
||||
|
||||
<div className="card-body py-3">
|
||||
<div className="align-box-row align-items-start justify-content-between">
|
||||
<div className="d-flex justify-content-between w-100">
|
||||
<span className="mb-0 font-size-sm">TPS history</span>
|
||||
|
||||
<div className="font-size-sm">
|
||||
{SERIES.map((key) => (
|
||||
<button
|
||||
key={key}
|
||||
onClick={() => setSeries(key)}
|
||||
className={classNames("btn btn-sm btn-white ml-2", {
|
||||
active: series === key,
|
||||
})}
|
||||
>
|
||||
{SERIES_INFO[key].interval}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div
|
||||
id="perf-history"
|
||||
className="mt-3 d-flex justify-content-end flex-row w-100"
|
||||
>
|
||||
<div className="w-100">
|
||||
<Bar data={chartData} options={chartOptions} height={80} />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
function AnimatedTransactionCount({ info }: { info: PerformanceInfo }) {
|
||||
const txCountRef = React.useRef(0);
|
||||
const countUpRef = React.useRef({ start: 0, period: 0, lastUpdate: 0 });
|
||||
const countUp = countUpRef.current;
|
||||
|
||||
const { transactionCount: txCount, avgTps } = info;
|
||||
|
||||
// Track last tx count to reset count up options
|
||||
if (txCount !== txCountRef.current) {
|
||||
if (countUp.lastUpdate > 0) {
|
||||
// Since we overshoot below, calculate the elapsed value
|
||||
// and start from there.
|
||||
const elapsed = Date.now() - countUp.lastUpdate;
|
||||
const elapsedPeriods = elapsed / (PERF_UPDATE_SEC * 1000);
|
||||
countUp.start = countUp.start + elapsedPeriods * countUp.period;
|
||||
countUp.period = txCount - countUp.start;
|
||||
} else {
|
||||
// Since this is the first tx count value, estimate the previous
|
||||
// tx count in order to have a starting point for our animation
|
||||
countUp.period = PERF_UPDATE_SEC * avgTps;
|
||||
countUp.start = txCount - countUp.period;
|
||||
}
|
||||
countUp.lastUpdate = Date.now();
|
||||
txCountRef.current = txCount;
|
||||
}
|
||||
|
||||
// Overshoot the target tx count in case the next update is delayed
|
||||
const COUNT_PERIODS = 3;
|
||||
const countUpEnd = countUp.start + COUNT_PERIODS * countUp.period;
|
||||
return (
|
||||
<CountUp
|
||||
start={countUp.start}
|
||||
end={countUpEnd}
|
||||
duration={PERF_UPDATE_SEC * COUNT_PERIODS}
|
||||
delay={0}
|
||||
useEasing={false}
|
||||
preserveValue={true}
|
||||
separator=","
|
||||
/>
|
||||
);
|
||||
}
|
@@ -1,16 +1,14 @@
|
||||
import React from "react";
|
||||
import CountUp from "react-countup";
|
||||
|
||||
import { TableCardBody } from "components/common/TableCardBody";
|
||||
import {
|
||||
useDashboardInfo,
|
||||
usePerformanceInfo,
|
||||
PERF_UPDATE_SEC,
|
||||
useSetActive,
|
||||
PerformanceInfo,
|
||||
} from "providers/stats/solanaBeach";
|
||||
import { slotsToHumanString } from "utils";
|
||||
import { useCluster, Cluster } from "providers/cluster";
|
||||
import { TpsCard } from "components/TpsCard";
|
||||
|
||||
export function ClusterStatsPage() {
|
||||
return (
|
||||
@@ -25,6 +23,7 @@ export function ClusterStatsPage() {
|
||||
</div>
|
||||
<StatsCardBody />
|
||||
</div>
|
||||
<TpsCard />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -71,8 +70,6 @@ function StatsCardBody() {
|
||||
slotsInEpoch - slotIndex,
|
||||
hourlyBlockTime
|
||||
);
|
||||
const averageTps = Math.round(performanceInfo.avgTPS);
|
||||
const transactionCount = <AnimatedTransactionCount info={performanceInfo} />;
|
||||
const blockHeight = epochInfo.blockHeight.toLocaleString("en-US");
|
||||
const currentSlot = epochInfo.absoluteSlot.toLocaleString("en-US");
|
||||
|
||||
@@ -102,56 +99,6 @@ function StatsCardBody() {
|
||||
<td className="w-100">Epoch time remaining</td>
|
||||
<td className="text-lg-right text-monospace">{epochTimeRemaining} </td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td className="w-100">Transaction count</td>
|
||||
<td className="text-lg-right text-monospace">{transactionCount} </td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td className="w-100">Transactions per second</td>
|
||||
<td className="text-lg-right text-monospace">{averageTps} </td>
|
||||
</tr>
|
||||
</TableCardBody>
|
||||
);
|
||||
}
|
||||
|
||||
function AnimatedTransactionCount({ info }: { info: PerformanceInfo }) {
|
||||
const txCountRef = React.useRef(0);
|
||||
const countUpRef = React.useRef({ start: 0, period: 0, lastUpdate: 0 });
|
||||
const countUp = countUpRef.current;
|
||||
|
||||
const { totalTransactionCount: txCount, avgTPS } = info;
|
||||
|
||||
// Track last tx count to reset count up options
|
||||
if (txCount !== txCountRef.current) {
|
||||
if (countUp.lastUpdate > 0) {
|
||||
// Since we overshoot below, calculate the elapsed value
|
||||
// and start from there.
|
||||
const elapsed = Date.now() - countUp.lastUpdate;
|
||||
const elapsedPeriods = elapsed / (PERF_UPDATE_SEC * 1000);
|
||||
countUp.start = countUp.start + elapsedPeriods * countUp.period;
|
||||
countUp.period = txCount - countUp.start;
|
||||
} else {
|
||||
// Since this is the first tx count value, estimate the previous
|
||||
// tx count in order to have a starting point for our animation
|
||||
countUp.period = PERF_UPDATE_SEC * avgTPS;
|
||||
countUp.start = txCount - countUp.period;
|
||||
}
|
||||
countUp.lastUpdate = Date.now();
|
||||
txCountRef.current = txCount;
|
||||
}
|
||||
|
||||
// Overshoot the target tx count in case the next update is delayed
|
||||
const COUNT_PERIODS = 3;
|
||||
const countUpEnd = countUp.start + COUNT_PERIODS * countUp.period;
|
||||
return (
|
||||
<CountUp
|
||||
start={countUp.start}
|
||||
end={countUpEnd}
|
||||
duration={PERF_UPDATE_SEC * COUNT_PERIODS}
|
||||
delay={0}
|
||||
useEasing={false}
|
||||
preserveValue={true}
|
||||
separator=","
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
@@ -1,7 +1,7 @@
|
||||
import React from "react";
|
||||
import io from "socket.io-client";
|
||||
|
||||
import { pick, number, is, StructType } from "superstruct";
|
||||
import { pick, array, nullable, number, is, StructType } from "superstruct";
|
||||
import { useCluster, Cluster } from "providers/cluster";
|
||||
|
||||
const DashboardInfo = pick({
|
||||
@@ -24,6 +24,11 @@ export const PERF_UPDATE_SEC = 5;
|
||||
|
||||
const PerformanceInfo = pick({
|
||||
avgTPS: number(),
|
||||
perfHistory: pick({
|
||||
s: array(nullable(number())),
|
||||
m: array(nullable(number())),
|
||||
l: array(nullable(number())),
|
||||
}),
|
||||
totalTransactionCount: number(),
|
||||
});
|
||||
|
||||
@@ -42,7 +47,17 @@ const DashboardContext = React.createContext<DashboardState | undefined>(
|
||||
undefined
|
||||
);
|
||||
|
||||
export type PerformanceInfo = StructType<typeof PerformanceInfo>;
|
||||
export type PerformanceInfo = {
|
||||
avgTps: number;
|
||||
historyMaxTps: number;
|
||||
perfHistory: {
|
||||
short: (number | null)[];
|
||||
medium: (number | null)[];
|
||||
long: (number | null)[];
|
||||
};
|
||||
transactionCount: number;
|
||||
};
|
||||
|
||||
type PerformanceState = { info: PerformanceInfo | undefined };
|
||||
const PerformanceContext = React.createContext<PerformanceState | undefined>(
|
||||
undefined
|
||||
@@ -87,7 +102,39 @@ export function SolanaBeachProvider({ children }: Props) {
|
||||
});
|
||||
socket.on("performanceInfo", (data: any) => {
|
||||
if (is(data, PerformanceInfo)) {
|
||||
setPerformanceInfo(data);
|
||||
const trimSeries = (series: (number | null)[]) => {
|
||||
return series.slice(series.length - 51, series.length - 1);
|
||||
};
|
||||
|
||||
const seriesMax = (series: (number | null)[]) => {
|
||||
return series.reduce((max: number, next) => {
|
||||
if (next === null) return max;
|
||||
return Math.max(max, next);
|
||||
}, 0);
|
||||
};
|
||||
|
||||
const normalize = (series: Array<number | null>, seconds: number) => {
|
||||
return series.map((next) => {
|
||||
if (next === null) return next;
|
||||
return Math.round(next / seconds);
|
||||
});
|
||||
};
|
||||
|
||||
const short = normalize(trimSeries(data.perfHistory.s), 15);
|
||||
const medium = normalize(trimSeries(data.perfHistory.m), 60);
|
||||
const long = normalize(trimSeries(data.perfHistory.l), 180);
|
||||
const historyMaxTps = Math.max(
|
||||
seriesMax(short),
|
||||
seriesMax(medium),
|
||||
seriesMax(long)
|
||||
);
|
||||
|
||||
setPerformanceInfo({
|
||||
avgTps: data.avgTPS,
|
||||
historyMaxTps,
|
||||
perfHistory: { short, medium, long },
|
||||
transactionCount: data.totalTransactionCount,
|
||||
});
|
||||
}
|
||||
});
|
||||
socket.on("rootNotification", (data: any) => {
|
||||
|
@@ -198,3 +198,49 @@ h4.slot-pill {
|
||||
width: 100% !important;
|
||||
}
|
||||
}
|
||||
|
||||
#chartjs-tooltip {
|
||||
opacity: 1;
|
||||
position: absolute;
|
||||
-webkit-transition: all .1s ease;
|
||||
transition: all .1s ease;
|
||||
pointer-events: none;
|
||||
-webkit-transform: translate(-50%, -105%);
|
||||
transform: translate(-50%, -105%);
|
||||
|
||||
div.content {
|
||||
font-size: 12px;
|
||||
color: white;
|
||||
margin: 0px;
|
||||
background: rgba(0, 0, 0, 0.9);
|
||||
border-radius: 4px;
|
||||
text-align: center;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
div.label {
|
||||
padding: 10px;
|
||||
background: #111;
|
||||
}
|
||||
|
||||
div.value {
|
||||
padding: 10px;
|
||||
background: black;
|
||||
border-bottom: 1px solid #333;
|
||||
}
|
||||
|
||||
&:after {
|
||||
display: block;
|
||||
position: relative;
|
||||
width: 0;
|
||||
height: 0;
|
||||
left: 50%;
|
||||
top: -1px;
|
||||
border-right: 10px solid transparent;
|
||||
border-top: 10px solid #111;
|
||||
border-left: 10px solid transparent;
|
||||
-webkit-transform: translate(-50%, 0);
|
||||
transform: translate(-50%, 0);
|
||||
content:'';
|
||||
}
|
||||
}
|
||||
|
Reference in New Issue
Block a user