);
}
diff --git a/frontend/components/AppBar.jsx b/frontend/components/AppBar.jsx
index 3d4cc611..88a93b72 100644
--- a/frontend/components/AppBar.jsx
+++ b/frontend/components/AppBar.jsx
@@ -1,47 +1,120 @@
import React from "react";
+import logoUrl from "../assets/stellar-logo-white.svg";
+
+const SunIcon = () => (
+
+);
export default class AppBar extends React.Component {
+ constructor(props) {
+ super(props);
+ this.state = {
+ theme: document.documentElement.getAttribute("data-theme") || "dark",
+ };
+ }
+
+ toggleTheme() {
+ const theme = this.state.theme === "dark" ? "light" : "dark";
+ document.documentElement.setAttribute("data-theme", theme);
+ try {
+ localStorage.setItem("theme", theme);
+ } catch (e) {
+ // Storage can be unavailable (private browsing); the toggle still works.
+ }
+ // Charts listen for this to re-resolve their palette.
+ window.dispatchEvent(new Event("themechange"));
+ this.setState({ theme });
+ }
+
render() {
return (
-
-
-
-
-
Stellar.org Dashboard
-
-
-
-
+
+ {this.state.theme === "dark" ? : }
+
+
-
+
);
}
}
diff --git a/frontend/components/D3BarChart.jsx b/frontend/components/D3BarChart.jsx
index 56fdbe22..b753b1b8 100644
--- a/frontend/components/D3BarChart.jsx
+++ b/frontend/components/D3BarChart.jsx
@@ -1,150 +1,151 @@
-import React, { useEffect, useRef } from "react";
+import React, { useEffect, useRef, useState } from "react";
import * as d3 from "d3";
-
+import {
+ getChartTheme,
+ roundedTopRect,
+ createTooltip,
+ tooltipRow,
+} from "./ui/chartUtils.js";
+
+// Single-series bar chart (used for ledger close times).
export default function D3BarChart({
data,
width = 400,
height = 120,
- margin = { top: 10, right: 10, bottom: 30, left: 50 },
- colorScale,
+ margin = { top: 10, right: 10, bottom: 8, left: 50 },
tickFormat,
+ tooltipTitle,
+ valueFormat,
}) {
const svgRef = useRef();
+ // Charts re-render when the theme changes so D3 picks up the new palette.
+ const [themeTick, setThemeTick] = useState(0);
+
+ useEffect(() => {
+ const onThemeChange = () => setThemeTick((t) => t + 1);
+ window.addEventListener("themechange", onThemeChange);
+ return () => window.removeEventListener("themechange", onThemeChange);
+ }, []);
useEffect(() => {
if (!data || data.length === 0) return;
+ const theme = getChartTheme(svgRef.current);
const svg = d3.select(svgRef.current);
svg.selectAll("*").remove(); // Clear previous render
const innerWidth = width - margin.left - margin.right;
const innerHeight = height - margin.top - margin.bottom;
- // Flatten all values from all series for domain calculation
- const allValues = data.flatMap((series) => series.values);
- const xValues = allValues.map((d) => d.x);
- const yValues = allValues.map((d) => d.y);
+ const values = data[0].values;
+ const xValues = values.map((d) => d.x);
+ const yValues = values.map((d) => d.y);
- // Create scales - use scalePoint for fixed spacing instead of scaleBand
const xScale = d3
.scalePoint()
.domain(xValues)
.range([0, innerWidth])
- .padding(1.0); // Double the gap - more space from Y-axis
+ .padding(1.0);
const yScale = d3
.scaleLinear()
.domain([0, d3.max(yValues)])
.range([innerHeight, 0]);
- // Fixed bar dimensions
- const fixedBarWidth = 5; // Fixed 5px bar width
- const fixedGapWidth = 3; // Fixed 3px gap between bars
-
- // Create color scale - match original react-d3-components colors
- const colors =
- colorScale ||
- d3
- .scaleOrdinal()
- .range([
- "#1f77b4",
- "#ff7f0e",
- "#2ca02c",
- "#d62728",
- "#9467bd",
- "#8c564b",
- "#e377c2",
- "#7f7f7f",
- "#bcbd22",
- "#17becf",
- ]);
-
- // Create main group
+ const barWidth = 5;
+
const g = svg
.attr("width", width)
.attr("height", height)
.append("g")
.attr("transform", `translate(${margin.left},${margin.top})`);
- // Calculate bar width - always use fixed width for all bars
- const barWidth = fixedBarWidth; // All bars are exactly 5px wide
-
- // Add bars for each series
- data.forEach((series, seriesIndex) => {
- g.selectAll(`.bar-${seriesIndex}`)
- .data(series.values)
- .enter()
- .append("rect")
- .attr("class", `bar-${seriesIndex}`)
- .attr("x", (d) => xScale(d.x) - fixedBarWidth / 2) // Center the bar on the scale point
- .attr("y", (d) => yScale(d.y))
- .attr("width", barWidth) // Always 5px wide
- .attr("height", (d) => innerHeight - yScale(d.y))
- .attr("fill", colors(seriesIndex))
- .style("shape-rendering", "crispEdges"); // Crisp edges like original
- });
-
- // Add x-axis with styling to match original
- const xAxis = d3.axisBottom(xScale).tickSize(6).tickPadding(3);
-
- const xAxisGroup = g
- .append("g")
- .attr("class", "axis")
- .attr("transform", `translate(0,${innerHeight})`)
- .call(xAxis);
-
- // Style x-axis to match original
- xAxisGroup
- .selectAll("text")
- .style("font-size", "10px")
- .style("font-family", "sans-serif")
- .style("fill", "#000");
-
- xAxisGroup
+ // Recessive horizontal gridlines carry the scale; no axis spines.
+ const yTickValues = yScale.ticks(4);
+ g.append("g")
+ .attr("class", "chart-grid")
.selectAll("line")
- .style("stroke", "#000")
- .style("shape-rendering", "crispEdges");
-
- xAxisGroup
- .select(".domain")
- .style("stroke", "#000")
- .style("shape-rendering", "crispEdges");
-
- // Add y-axis with styling to match original
+ .data(yTickValues)
+ .enter()
+ .append("line")
+ .attr("x1", 0)
+ .attr("x2", innerWidth)
+ .attr("y1", (d) => yScale(d))
+ .attr("y2", (d) => yScale(d))
+ .style("stroke", theme.grid);
+
+ // Bars: thin marks with rounded data-ends anchored to the baseline.
+ g.selectAll(".bar")
+ .data(values)
+ .enter()
+ .append("path")
+ .attr("class", "bar")
+ .attr("d", (d) =>
+ roundedTopRect(
+ xScale(d.x) - barWidth / 2,
+ yScale(d.y),
+ barWidth,
+ innerHeight - yScale(d.y),
+ 2,
+ ),
+ )
+ .attr("fill", theme.primary);
+
+ // Y axis: text only.
const yAxis = d3
.axisLeft(yScale)
- .tickSize(6)
- .tickPadding(3)
- .ticks(Math.min(7, Math.floor(d3.max(yValues)))) // Limit to max 7 ticks or the max value, whichever is smaller
- .tickValues(
- d3.range(0, Math.ceil(d3.max(yValues)) + 1).filter((d) => d % 1 === 0),
- ); // Only integer values
-
- if (tickFormat) {
- yAxis.tickFormat(tickFormat);
- } else {
- yAxis.tickFormat(d3.format("d")); // Format as integers (1, 2, 3) instead of decimals (1.0, 2.0)
- }
+ .tickSize(0)
+ .tickPadding(8)
+ .tickValues(yTickValues)
+ .tickFormat(tickFormat || d3.format("d"));
const yAxisGroup = g.append("g").attr("class", "axis").call(yAxis);
-
- // Style y-axis to match original
- yAxisGroup
- .selectAll("text")
- .style("font-size", "10px")
- .style("font-family", "sans-serif")
- .style("fill", "#000");
-
- yAxisGroup
- .selectAll("line")
- .style("stroke", "#000")
- .style("shape-rendering", "crispEdges");
-
- yAxisGroup
- .select(".domain")
- .style("stroke", "#000")
- .style("shape-rendering", "crispEdges");
- }, [data, width, height, margin, colorScale, tickFormat]);
-
- return
;
+ yAxisGroup.select(".domain").remove();
+ yAxisGroup.selectAll("text").style("fill", theme.axisText);
+
+ // Hover layer: full-height hit targets, one per bar.
+ const tooltip = createTooltip();
+ const step = Math.max(innerWidth / Math.max(xValues.length, 1), barWidth);
+ g.selectAll(".hit")
+ .data(values)
+ .enter()
+ .append("rect")
+ .attr("class", "hit")
+ .attr("x", (d) => xScale(d.x) - step / 2)
+ .attr("y", 0)
+ .attr("width", step)
+ .attr("height", innerHeight)
+ .attr("fill", "transparent")
+ .on("mousemove", (event, d) => {
+ const title = tooltipTitle ? tooltipTitle(d.x) : d.x;
+ const value = valueFormat ? valueFormat(d.y) : d.y;
+ tooltip.show(
+ `
${title}
` +
+ tooltipRow(theme.primary, data[0].label, value),
+ event.clientX,
+ event.clientY,
+ );
+ })
+ .on("mouseleave", () => tooltip.hide());
+
+ return () => tooltip.destroy();
+ }, [
+ data,
+ width,
+ height,
+ margin,
+ tickFormat,
+ tooltipTitle,
+ valueFormat,
+ themeTick,
+ ]);
+
+ return (
+
+ );
}
diff --git a/frontend/components/D3BarChartNoXLabels.jsx b/frontend/components/D3BarChartNoXLabels.jsx
index 48a65f13..6742b67d 100644
--- a/frontend/components/D3BarChartNoXLabels.jsx
+++ b/frontend/components/D3BarChartNoXLabels.jsx
@@ -1,207 +1,238 @@
-import React, { useEffect, useRef } from "react";
+import React, { useEffect, useRef, useState } from "react";
import * as d3 from "d3";
-
+import {
+ getChartTheme,
+ roundedTopRect,
+ createTooltip,
+ tooltipRow,
+} from "./ui/chartUtils.js";
+
+// Stacked two-series bar chart without x labels (txs/ops, successful/failed).
export default function D3BarChartNoXLabels({
data,
width = 400,
height = 120,
- margin = { top: 10, right: 10, bottom: 8, left: 50 }, // Reduced bottom margin since no X labels
- colorScale,
+ margin = { top: 10, right: 10, bottom: 8, left: 50 },
tickFormat,
- yAxisMax = 450, // Maximum Y value
- yAxisStep = 50, // Y axis increment step
+ yAxisMax = 450,
+ yAxisStep = 50,
+ tooltipTitle,
+ valueFormat,
+ xLabelEvery = 0, // 0 = no x labels; N = label every Nth point
}) {
const svgRef = useRef();
+ // Charts re-render when the theme changes so D3 picks up the new palette.
+ const [themeTick, setThemeTick] = useState(0);
+
+ useEffect(() => {
+ const onThemeChange = () => setThemeTick((t) => t + 1);
+ window.addEventListener("themechange", onThemeChange);
+ return () => window.removeEventListener("themechange", onThemeChange);
+ }, []);
useEffect(() => {
if (!data || data.length === 0) return;
+ const theme = getChartTheme(svgRef.current);
+ const seriesColors = [theme.seriesA, theme.seriesB];
const svg = d3.select(svgRef.current);
svg.selectAll("*").remove(); // Clear previous render
const innerWidth = width - margin.left - margin.right;
const innerHeight = height - margin.top - margin.bottom;
- // Flatten all values from all series for domain calculation
- const allValues = data.flatMap((series) => series.values);
- const xValues = allValues.map((d) => d.x);
- const yValues = allValues.map((d) => d.y);
+ const xValues = data[0].values.map((d) => d.x);
- // Create scales - use scalePoint for fixed spacing instead of scaleBand
const xScale = d3
.scalePoint()
.domain(xValues)
.range([0, innerWidth])
- .padding(1.0); // Double the gap - more space from Y-axis
+ .padding(1.0);
const yScale = d3
.scaleLinear()
- .domain([0, yAxisMax]) // Use fixed max instead of data max
+ .domain([0, yAxisMax])
.range([innerHeight, 0]);
- // Fixed bar dimensions
- const fixedBarWidth = 5; // Fixed 5px bar width
- const fixedGapWidth = 3; // Fixed 3px gap between bars
-
- // Create color scale - match original react-d3-components colors
- const colors =
- colorScale ||
- d3
- .scaleOrdinal()
- .range([
- "#1f77b4",
- "#ff7f0e",
- "#2ca02c",
- "#d62728",
- "#9467bd",
- "#8c564b",
- "#e377c2",
- "#7f7f7f",
- "#bcbd22",
- "#17becf",
- ]);
-
- // Create main group
+ const barWidth = 5;
+ const segmentGap = 2; // surface gap between stacked segments
+
const g = svg
.attr("width", width)
.attr("height", height)
.append("g")
.attr("transform", `translate(${margin.left},${margin.top})`);
- // Calculate bar width - always use fixed width for all bars
- const barWidth = fixedBarWidth; // All bars are exactly 5px wide
-
- // Add bars for each series - create stacked bars
- // First, we need to calculate the cumulative values for stacking
+ // Recessive horizontal gridlines carry the scale; no axis spines.
+ // Let D3 pick round tick values within the fixed domain.
+ const yTickValues = yScale.ticks(4);
+ g.append("g")
+ .attr("class", "chart-grid")
+ .selectAll("line")
+ .data(yTickValues)
+ .enter()
+ .append("line")
+ .attr("x1", 0)
+ .attr("x2", innerWidth)
+ .attr("y1", (d) => yScale(d))
+ .attr("y2", (d) => yScale(d))
+ .style("stroke", theme.grid);
+
+ // Stack the two series (bottom = series 0, top = series 1).
const stackedData = [];
-
- // Assume we have exactly 2 series for stacking
if (data.length === 2) {
- const xValues = data[0].values.map((d) => d.x);
-
xValues.forEach((x, index) => {
- const bottomValue = data[0].values[index].y; // First series (bottom)
- const topValue = data[1].values[index].y; // Second series (top)
-
stackedData.push({
x: x,
- bottom: bottomValue,
- top: topValue,
- bottomHeight: bottomValue,
- topHeight: topValue,
- totalHeight: bottomValue + topValue,
+ bottom: data[0].values[index].y,
+ top: data[1].values[index].y,
});
});
- // Draw bottom bars (first series)
- g.selectAll(".bar-bottom")
- .data(stackedData)
- .enter()
- .append("rect")
- .attr("class", "bar-bottom")
- .attr("x", (d) => xScale(d.x) - fixedBarWidth / 2)
- .attr("y", (d) => yScale(d.bottomHeight))
- .attr("width", barWidth)
- .attr("height", (d) => innerHeight - yScale(d.bottomHeight))
- .attr("fill", colors(0))
- .style("shape-rendering", "crispEdges");
-
- // Draw top bars (second series) - stacked on top of bottom bars
- g.selectAll(".bar-top")
+ const bars = g
+ .selectAll(".bar-group")
.data(stackedData)
.enter()
- .append("rect")
- .attr("class", "bar-top")
- .attr("x", (d) => xScale(d.x) - fixedBarWidth / 2)
- .attr("y", (d) => yScale(d.totalHeight))
- .attr("width", barWidth)
- .attr("height", (d) => yScale(d.bottomHeight) - yScale(d.totalHeight))
- .attr("fill", colors(1))
- .style("shape-rendering", "crispEdges");
+ .append("g")
+ .attr("class", "bar-group");
+
+ // Bottom segment: flat unless it is the data end, rounded when alone.
+ bars
+ .append("path")
+ .attr("d", (d) => {
+ const x = xScale(d.x) - barWidth / 2;
+ const yTop = yScale(d.bottom);
+ const h = innerHeight - yTop;
+ if (d.top > 0) {
+ return h > 0
+ ? `M${x},${yTop} H${x + barWidth} V${innerHeight} H${x} Z`
+ : "";
+ }
+ return roundedTopRect(x, yTop, barWidth, h, 2);
+ })
+ .attr("fill", seriesColors[0]);
+
+ // Top segment: rounded data end, separated by a surface gap.
+ bars
+ .append("path")
+ .attr("d", (d) => {
+ if (d.top <= 0) {
+ return "";
+ }
+ const x = xScale(d.x) - barWidth / 2;
+ const yTotal = yScale(d.bottom + d.top);
+ const yBottom = yScale(d.bottom);
+ const h = Math.max(yBottom - yTotal - segmentGap, 0.5);
+ return roundedTopRect(x, yTotal, barWidth, h, 2);
+ })
+ .attr("fill", seriesColors[1]);
} else {
- // Fallback to original behavior for non-stacked charts
+ // Fallback for non-stacked charts.
data.forEach((series, seriesIndex) => {
g.selectAll(`.bar-${seriesIndex}`)
.data(series.values)
.enter()
- .append("rect")
+ .append("path")
.attr("class", `bar-${seriesIndex}`)
- .attr("x", (d) => xScale(d.x) - fixedBarWidth / 2)
- .attr("y", (d) => yScale(d.y))
- .attr("width", barWidth)
- .attr("height", (d) => innerHeight - yScale(d.y))
- .attr("fill", colors(seriesIndex))
- .style("shape-rendering", "crispEdges");
+ .attr("d", (d) =>
+ roundedTopRect(
+ xScale(d.x) - barWidth / 2,
+ yScale(d.y),
+ barWidth,
+ innerHeight - yScale(d.y),
+ 2,
+ ),
+ )
+ .attr("fill", seriesColors[seriesIndex % seriesColors.length]);
});
}
- // Add x-axis with NO labels
- const xAxis = d3
- .axisBottom(xScale)
- .tickSize(6)
- .tickPadding(3)
- .tickFormat(""); // No labels
-
- const xAxisGroup = g
- .append("g")
- .attr("class", "axis")
- .attr("transform", `translate(0,${innerHeight})`)
- .call(xAxis);
-
- // Style x-axis lines only (no text)
- xAxisGroup
- .selectAll("line")
- .style("stroke", "#000")
- .style("shape-rendering", "crispEdges");
-
- xAxisGroup
- .select(".domain")
- .style("stroke", "#000")
- .style("shape-rendering", "crispEdges");
-
- // Add y-axis with custom ticks
- const yAxisTicks = d3.range(0, yAxisMax + yAxisStep, yAxisStep); // [0, 50, 100, 150, ..., yAxisMax]
-
+ // Y axis: text only.
const yAxis = d3
.axisLeft(yScale)
- .tickSize(6)
- .tickPadding(3)
- .tickValues(yAxisTicks);
-
- if (tickFormat) {
- yAxis.tickFormat(tickFormat);
- } else {
- yAxis.tickFormat(d3.format("d")); // Format as integers
- }
+ .tickSize(0)
+ .tickPadding(8)
+ .tickValues(yTickValues)
+ .tickFormat(tickFormat || d3.format("d"));
const yAxisGroup = g.append("g").attr("class", "axis").call(yAxis);
+ yAxisGroup.select(".domain").remove();
+ yAxisGroup.selectAll("text").style("fill", theme.axisText);
+
+ // Optional sparse x labels (e.g. dates on the 30-day chart).
+ if (xLabelEvery > 0) {
+ const xTickValues = xValues.filter((_, i) => i % xLabelEvery === 0);
+ const xAxis = d3
+ .axisBottom(xScale)
+ .tickSize(0)
+ .tickPadding(8)
+ .tickValues(xTickValues);
+
+ const xAxisGroup = g
+ .append("g")
+ .attr("class", "axis")
+ .attr("transform", `translate(0,${innerHeight})`)
+ .call(xAxis);
+ xAxisGroup.select(".domain").remove();
+ xAxisGroup.selectAll("text").style("fill", theme.axisText);
+ }
- // Style y-axis to match original
- yAxisGroup
- .selectAll("text")
- .style("font-size", "10px")
- .style("font-family", "sans-serif")
- .style("fill", "#000");
-
- yAxisGroup
- .selectAll("line")
- .style("stroke", "#000")
- .style("shape-rendering", "crispEdges");
+ // Hover layer: full-height hit targets, one per bar.
+ const tooltip = createTooltip();
+ if (data.length === 2) {
+ const step = Math.max(
+ innerWidth / Math.max(xValues.length, 1),
+ barWidth,
+ );
+ g.selectAll(".hit")
+ .data(stackedData)
+ .enter()
+ .append("rect")
+ .attr("class", "hit")
+ .attr("x", (d) => xScale(d.x) - step / 2)
+ .attr("y", 0)
+ .attr("width", step)
+ .attr("height", innerHeight)
+ .attr("fill", "transparent")
+ .on("mousemove", (event, d) => {
+ const title = tooltipTitle ? tooltipTitle(d.x) : d.x;
+ const fmt = valueFormat || ((v) => v.toLocaleString("en-US"));
+ tooltip.show(
+ `
${title}
` +
+ tooltipRow(seriesColors[0], data[0].label, fmt(d.bottom)) +
+ tooltipRow(seriesColors[1], data[1].label, fmt(d.top)),
+ event.clientX,
+ event.clientY,
+ );
+ })
+ .on("mouseleave", () => tooltip.hide());
+ }
- yAxisGroup
- .select(".domain")
- .style("stroke", "#000")
- .style("shape-rendering", "crispEdges");
+ return () => tooltip.destroy();
}, [
data,
width,
height,
margin,
- colorScale,
tickFormat,
yAxisMax,
yAxisStep,
+ tooltipTitle,
+ valueFormat,
+ xLabelEvery,
+ themeTick,
]);
- return
;
+ return (
+
s.label).join(" and ")} bar chart`
+ : "chart"
+ }
+ >
+ );
}
diff --git a/frontend/components/FailedTransactionsChart.jsx b/frontend/components/FailedTransactionsChart.jsx
index 34fc5dbc..a61aa5a5 100644
--- a/frontend/components/FailedTransactionsChart.jsx
+++ b/frontend/components/FailedTransactionsChart.jsx
@@ -1,30 +1,15 @@
import React from "react";
-import Panel from "muicss/lib/react/panel";
import axios from "axios";
import * as d3 from "d3";
import D3BarChartNoXLabels from "./D3BarChartNoXLabels.jsx";
import clone from "lodash/clone";
import each from "lodash/each";
+import Card from "./ui/Card.jsx";
export default class FailedTransactionsChart extends React.Component {
constructor(props) {
super(props);
this.panel = null;
- // Use the same colors as the original react-d3-components
- this.colorScale = d3
- .scaleOrdinal()
- .range([
- "#1f77b4",
- "#ff7f0e",
- "#2ca02c",
- "#d62728",
- "#9467bd",
- "#8c564b",
- "#e377c2",
- "#7f7f7f",
- "#bcbd22",
- "#17becf",
- ]);
this.state = {
loading: true,
chartWidth: 400,
@@ -33,17 +18,25 @@ export default class FailedTransactionsChart extends React.Component {
yAxisStep: 100, // Default value, will be updated dynamically
};
this.url = `${this.props.horizonURL}/ledgers?order=desc&limit=${this.props.limit}`;
+ this.tooltipTitle = (x) => `Ledger #${x}`;
}
componentDidMount() {
this.getLedgers();
// Update chart width
this.updateSize();
- setInterval(() => this.updateSize(), 5000);
+ this.sizeInterval = setInterval(() => this.updateSize(), 5000);
+ }
+
+ componentWillUnmount() {
+ clearInterval(this.sizeInterval);
+ if (this.newLedgerListener) {
+ this.newLedgerListener.remove();
+ }
}
updateSize() {
- let value = this.panel.offsetWidth - 20;
+ let value = this.panel.offsetWidth - 42;
if (this.state.chartWidth != value) {
this.setState({ chartWidth: value });
}
@@ -79,7 +72,7 @@ export default class FailedTransactionsChart extends React.Component {
// Determine step size based on network type
let stepSize;
- if (this.props.network === "Test network") {
+ if (this.props.network === "Testnet") {
stepSize = 1; // Test network uses step size of 1
} else {
// Live network: choose between 50 and 100 based on resulting tick count
@@ -91,7 +84,7 @@ export default class FailedTransactionsChart extends React.Component {
// Ensure minimum values for better chart readability
let minYAxisMax;
- if (this.props.network === "Test network") {
+ if (this.props.network === "Testnet") {
minYAxisMax = 10; // Smaller minimum for test network
} else {
minYAxisMax = stepSize === 50 ? 100 : 200;
@@ -126,11 +119,11 @@ export default class FailedTransactionsChart extends React.Component {
axios.get(this.url).then((response) => {
let data = [
{
- label: "Success",
+ label: "Successful",
values: [],
},
{
- label: "Fail",
+ label: "Failed",
values: [],
},
];
@@ -150,7 +143,7 @@ export default class FailedTransactionsChart extends React.Component {
this.setState({ loading: false, data, yAxisMax, yAxisStep });
// Start listening to events
- this.props.emitter.addListener(
+ this.newLedgerListener = this.props.emitter.addListener(
this.props.newLedgerEventName,
this.onNewLedger.bind(this),
);
@@ -164,33 +157,38 @@ export default class FailedTransactionsChart extends React.Component {
this.panel = el;
}}
>
-
-
-
- Successful
- {" "}
- &{" "}
-
Failed {" "}
- Txs in the last {this.props.limit} ledgers: {this.props.network}
-
- API
-
-
+
+
+ Successful
+
+
+ Failed
+
+
+ }
+ >
{this.state.loading ? (
- "Loading..."
+