mirror of
https://github.com/gosticks/web.git
synced 2026-10-06 06:37:07 +00:00
Convert ClientsGraph to use WithAPIData
Signed-off-by: Mcat12 <newtoncat12@yahoo.com>
This commit is contained in:
@@ -10,149 +10,78 @@
|
||||
|
||||
import React, { Component } from "react";
|
||||
import ReactDOM from "react-dom";
|
||||
import PropTypes from "prop-types";
|
||||
import { Line } from "react-chartjs-2";
|
||||
import { translate } from "react-i18next";
|
||||
import { padNumber, makeCancelable, ignoreCancel } from "../../util";
|
||||
import { padNumber } from "../../util";
|
||||
import api from "../../util/api";
|
||||
import ChartTooltip from "./ChartTooltip";
|
||||
import { WithAPIData } from "../common/WithAPIData";
|
||||
|
||||
class ClientsGraph extends Component {
|
||||
state = {
|
||||
loading: true,
|
||||
data: {
|
||||
labels: [],
|
||||
datasets: []
|
||||
}
|
||||
static propTypes = {
|
||||
loading: PropTypes.bool.isRequired,
|
||||
labels: PropTypes.array.isRequired,
|
||||
datasets: PropTypes.array.isRequired
|
||||
};
|
||||
|
||||
static graphOptions = {
|
||||
tooltips: {
|
||||
enabled: false,
|
||||
mode: "x-axis",
|
||||
custom: () => "placeholder",
|
||||
itemSort: function(a, b) {
|
||||
return b.yLabel - a.yLabel;
|
||||
},
|
||||
callbacks: {
|
||||
title: tooltipItem => {
|
||||
const time = tooltipItem[0].xLabel.match(/(\d?\d):?(\d?\d?)/);
|
||||
const hour = parseInt(time[1], 10);
|
||||
const minute = parseInt(time[2], 10) || 0;
|
||||
const from = padNumber(hour) + ":" + padNumber(minute - 5) + ":00";
|
||||
const to = padNumber(hour) + ":" + padNumber(minute + 4) + ":59";
|
||||
|
||||
return t("Client activity from {{from}} to {{to}}", { from, to });
|
||||
},
|
||||
label: (tooltipItems, data) => {
|
||||
return (
|
||||
data.datasets[tooltipItems.datasetIndex].label +
|
||||
": " +
|
||||
tooltipItems.yLabel
|
||||
);
|
||||
}
|
||||
}
|
||||
},
|
||||
legend: { display: false },
|
||||
scales: {
|
||||
xAxes: [
|
||||
{
|
||||
type: "time",
|
||||
time: {
|
||||
unit: "hour",
|
||||
displayFormats: { hour: "HH:mm" },
|
||||
tooltipFormat: "HH:mm"
|
||||
}
|
||||
}
|
||||
],
|
||||
yAxes: [
|
||||
{
|
||||
ticks: { beginAtZero: true },
|
||||
stacked: true
|
||||
}
|
||||
]
|
||||
},
|
||||
maintainAspectRatio: false
|
||||
};
|
||||
|
||||
constructor(props) {
|
||||
super(props);
|
||||
this.graphRef = React.createRef();
|
||||
this.updateGraph = this.updateGraph.bind(this);
|
||||
}
|
||||
|
||||
updateGraph() {
|
||||
this.updateHandler = makeCancelable(api.getClientsGraph(), {
|
||||
repeat: this.updateGraph,
|
||||
interval: 10 * 60 * 1000
|
||||
});
|
||||
this.updateHandler.promise
|
||||
.then(res => {
|
||||
// Remove last data point as it's not yet finished
|
||||
res.over_time.splice(-1, 1);
|
||||
|
||||
const colors = [
|
||||
"#20a8d8",
|
||||
"#f86c6b",
|
||||
"#4dbd74",
|
||||
"#f8cb00",
|
||||
"#263238",
|
||||
"#63c2de",
|
||||
"#b0bec5"
|
||||
];
|
||||
const labels = res.over_time.map(
|
||||
step => new Date(1000 * step.timestamp)
|
||||
);
|
||||
const datasets = [];
|
||||
|
||||
// Fill in dataset metadata
|
||||
let i = 0;
|
||||
for (let client of res.clients) {
|
||||
datasets.push({
|
||||
label: client.name.length !== 0 ? client.name : client.ip,
|
||||
// If we ran out of colors, make a random one
|
||||
backgroundColor:
|
||||
i < colors.length
|
||||
? colors[i]
|
||||
: "#" +
|
||||
parseInt("" + Math.random() * 0xffffff, 10)
|
||||
.toString(16)
|
||||
.padStart(6, "0"),
|
||||
pointRadius: 0,
|
||||
pointHitRadius: 5,
|
||||
pointHoverRadius: 5,
|
||||
cubicInterpolationMode: "monotone",
|
||||
data: []
|
||||
});
|
||||
|
||||
i++;
|
||||
}
|
||||
|
||||
// Fill in data & labels
|
||||
for (let step of res.over_time) {
|
||||
for (let destination in datasets) {
|
||||
if (datasets.hasOwnProperty(destination))
|
||||
datasets[destination].data.push(step.data[destination]);
|
||||
}
|
||||
}
|
||||
|
||||
datasets.sort((a, b) => a.label.localeCompare(b.label));
|
||||
|
||||
this.setState({ data: { labels, datasets }, loading: false });
|
||||
})
|
||||
.catch(ignoreCancel);
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
this.updateGraph();
|
||||
}
|
||||
|
||||
componentWillUnmount() {
|
||||
this.updateHandler.cancel();
|
||||
}
|
||||
|
||||
render() {
|
||||
const { t } = this.props;
|
||||
|
||||
const options = {
|
||||
tooltips: {
|
||||
enabled: false,
|
||||
mode: "x-axis",
|
||||
custom: () => "placeholder",
|
||||
itemSort: function(a, b) {
|
||||
return b.yLabel - a.yLabel;
|
||||
},
|
||||
callbacks: {
|
||||
title: tooltipItem => {
|
||||
const time = tooltipItem[0].xLabel.match(/(\d?\d):?(\d?\d?)/);
|
||||
const hour = parseInt(time[1], 10);
|
||||
const minute = parseInt(time[2], 10) || 0;
|
||||
const from = padNumber(hour) + ":" + padNumber(minute - 5) + ":00";
|
||||
const to = padNumber(hour) + ":" + padNumber(minute + 4) + ":59";
|
||||
|
||||
return t("Client activity from {{from}} to {{to}}", { from, to });
|
||||
},
|
||||
label: (tooltipItems, data) => {
|
||||
return (
|
||||
data.datasets[tooltipItems.datasetIndex].label +
|
||||
": " +
|
||||
tooltipItems.yLabel
|
||||
);
|
||||
}
|
||||
}
|
||||
},
|
||||
legend: { display: false },
|
||||
scales: {
|
||||
xAxes: [
|
||||
{
|
||||
type: "time",
|
||||
time: {
|
||||
unit: "hour",
|
||||
displayFormats: { hour: "HH:mm" },
|
||||
tooltipFormat: "HH:mm"
|
||||
}
|
||||
}
|
||||
],
|
||||
yAxes: [
|
||||
{
|
||||
ticks: { beginAtZero: true },
|
||||
stacked: true
|
||||
}
|
||||
]
|
||||
},
|
||||
maintainAspectRatio: false
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="card">
|
||||
<div className="card-header">{t("Clients Over Last 24 Hours")}</div>
|
||||
@@ -160,12 +89,16 @@ class ClientsGraph extends Component {
|
||||
<Line
|
||||
width={970}
|
||||
height={170}
|
||||
data={this.state.data}
|
||||
options={options}
|
||||
data={{
|
||||
labels: this.props.labels,
|
||||
datasets: this.props.datasets
|
||||
}}
|
||||
options={ClientsGraph.graphOptions}
|
||||
ref={this.graphRef}
|
||||
/>
|
||||
</div>
|
||||
{this.state.loading ? (
|
||||
|
||||
{this.props.loading ? (
|
||||
<div
|
||||
className="card-img-overlay"
|
||||
style={{ background: "rgba(255,255,255,0.7)" }}
|
||||
@@ -181,9 +114,13 @@ class ClientsGraph extends Component {
|
||||
/>
|
||||
</div>
|
||||
) : null}
|
||||
|
||||
{// Now you're thinking with portals!
|
||||
ReactDOM.createPortal(
|
||||
<ChartTooltip chart={this.graphRef} handler={options.tooltips} />,
|
||||
<ChartTooltip
|
||||
chart={this.graphRef}
|
||||
handler={ClientsGraph.graphOptions.tooltips}
|
||||
/>,
|
||||
document.body
|
||||
)}
|
||||
</div>
|
||||
@@ -191,4 +128,90 @@ class ClientsGraph extends Component {
|
||||
}
|
||||
}
|
||||
|
||||
export default translate("dashboard")(ClientsGraph);
|
||||
/**
|
||||
* Transform the API data into props for ClientsGraph
|
||||
*
|
||||
* @param data the API data
|
||||
* @returns {{labels: Date[], datasets: Array, loading: boolean}} ClientsGraph
|
||||
* props
|
||||
*/
|
||||
export const transformData = data => {
|
||||
// Remove last data point as it's not yet finished
|
||||
const overTime = data.over_time.slice(0, -1);
|
||||
|
||||
const colors = [
|
||||
"#20a8d8",
|
||||
"#f86c6b",
|
||||
"#4dbd74",
|
||||
"#f8cb00",
|
||||
"#263238",
|
||||
"#63c2de",
|
||||
"#b0bec5"
|
||||
];
|
||||
const labels = overTime.map(step => new Date(1000 * step.timestamp));
|
||||
const datasets = [];
|
||||
|
||||
// Fill in dataset metadata
|
||||
let i = 0;
|
||||
for (let client of data.clients) {
|
||||
datasets.push({
|
||||
label: client.name.length !== 0 ? client.name : client.ip,
|
||||
// If we ran out of colors, make a random one
|
||||
backgroundColor:
|
||||
i < colors.length
|
||||
? colors[i]
|
||||
: "#" +
|
||||
parseInt("" + Math.random() * 0xffffff, 10)
|
||||
.toString(16)
|
||||
.padStart(6, "0"),
|
||||
pointRadius: 0,
|
||||
pointHitRadius: 5,
|
||||
pointHoverRadius: 5,
|
||||
cubicInterpolationMode: "monotone",
|
||||
data: []
|
||||
});
|
||||
|
||||
i++;
|
||||
}
|
||||
|
||||
// Fill in data & labels
|
||||
for (let step of overTime) {
|
||||
for (let destination in datasets) {
|
||||
if (datasets.hasOwnProperty(destination))
|
||||
datasets[destination].data.push(step.data[destination]);
|
||||
}
|
||||
}
|
||||
|
||||
datasets.sort((a, b) => a.label.localeCompare(b.label));
|
||||
|
||||
return { labels, datasets, loading: false };
|
||||
};
|
||||
|
||||
/**
|
||||
* The props used to show a loading state (either initial load or error)
|
||||
*
|
||||
* @returns {*} the loading props
|
||||
*/
|
||||
export const loadingState = () => ({
|
||||
loading: true,
|
||||
labels: [],
|
||||
datasets: []
|
||||
});
|
||||
|
||||
export const TranslatedClientsGraph = translate("dashboard")(ClientsGraph);
|
||||
|
||||
export default props => (
|
||||
<WithAPIData
|
||||
apiCall={api.getClientsGraph}
|
||||
repeatOptions={{
|
||||
interval: 10 * 60 * 1000
|
||||
}}
|
||||
renderInitial={() => (
|
||||
<TranslatedClientsGraph {...loadingState()} {...props} />
|
||||
)}
|
||||
renderOk={data => (
|
||||
<TranslatedClientsGraph {...transformData(data)} {...props} />
|
||||
)}
|
||||
renderErr={() => <TranslatedClientsGraph {...loadingState()} {...props} />}
|
||||
/>
|
||||
);
|
||||
|
||||
@@ -10,10 +10,12 @@
|
||||
|
||||
import React from "react";
|
||||
import { shallow } from "enzyme";
|
||||
import fetchMock from "fetch-mock";
|
||||
import ClientsGraph from "./ClientsGraph";
|
||||
import {
|
||||
TranslatedClientsGraph,
|
||||
transformData,
|
||||
loadingState
|
||||
} from "./ClientsGraph";
|
||||
|
||||
const endpoint = "/admin/api/stats/overTime/clients";
|
||||
const fakeData = {
|
||||
over_time: [
|
||||
{ timestamp: 1513218354, data: [48476, 35688, 95153, 56971, 83497] },
|
||||
@@ -36,45 +38,27 @@ const fakeData = {
|
||||
]
|
||||
};
|
||||
|
||||
it("shows loading indicator before first load", () => {
|
||||
fetchMock.mock(endpoint, ignoreAPI);
|
||||
it("shows loading indicator correctly", () => {
|
||||
const wrapper = shallow(<TranslatedClientsGraph {...loadingState()} />);
|
||||
|
||||
const wrapper = shallow(<ClientsGraph />);
|
||||
|
||||
expect(wrapper.state().loading).toBeTruthy();
|
||||
expect(wrapper.children(".card-img-overlay")).toExist();
|
||||
});
|
||||
|
||||
it("hides loading indicator after first load", async () => {
|
||||
fetchMock.mock(endpoint, { body: fakeData });
|
||||
it("hides loading indicator correctly", async () => {
|
||||
const wrapper = shallow(
|
||||
<TranslatedClientsGraph {...loadingState()} loading={false} />
|
||||
);
|
||||
|
||||
const wrapper = shallow(<ClientsGraph />);
|
||||
|
||||
await tick();
|
||||
wrapper.update();
|
||||
|
||||
expect(wrapper.state().loading).toBeFalsy();
|
||||
expect(wrapper.children(".card-img-overlay")).not.toExist();
|
||||
});
|
||||
|
||||
it("loads API data correctly", async () => {
|
||||
fetchMock.mock(endpoint, { body: fakeData });
|
||||
const data = transformData(fakeData);
|
||||
|
||||
const wrapper = shallow(<ClientsGraph />);
|
||||
|
||||
await tick();
|
||||
wrapper.update();
|
||||
|
||||
expect(wrapper.state().data.labels[0]).toEqual(
|
||||
expect(data.labels[0]).toEqual(
|
||||
new Date(1000 * fakeData.over_time[0].timestamp)
|
||||
);
|
||||
expect(wrapper.state().data.datasets[2].label).toEqual(
|
||||
fakeData.clients[2].name
|
||||
);
|
||||
expect(wrapper.state().data.datasets[1].label).toEqual(
|
||||
fakeData.clients[1].ip
|
||||
);
|
||||
expect(wrapper.state().data.datasets[0].data.length).toEqual(
|
||||
fakeData.over_time.length - 1
|
||||
);
|
||||
expect(data.datasets[2].label).toEqual(fakeData.clients[2].name);
|
||||
expect(data.datasets[1].label).toEqual(fakeData.clients[1].ip);
|
||||
expect(data.datasets[0].data.length).toEqual(fakeData.over_time.length - 1);
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user