mirror of
https://github.com/gosticks/web.git
synced 2026-10-06 14:47:32 +00:00
Convert GenericDoughnutChart to use WithAPIData
Signed-off-by: Mcat12 <newtoncat12@yahoo.com>
This commit is contained in:
@@ -11,14 +11,15 @@
|
||||
import React, { Component } from "react";
|
||||
import { Doughnut } from "react-chartjs-2";
|
||||
import PropTypes from "prop-types";
|
||||
import { makeCancelable, ignoreCancel } from "../../util";
|
||||
import { WithAPIData } from "../common/WithAPIData";
|
||||
|
||||
class GenericDoughnutChart extends Component {
|
||||
state = {
|
||||
loading: true,
|
||||
data: [],
|
||||
colors: [],
|
||||
labels: []
|
||||
export class GenericDoughnutChart extends Component {
|
||||
static propTypes = {
|
||||
title: PropTypes.string.isRequired,
|
||||
loading: PropTypes.bool.isRequired,
|
||||
data: PropTypes.array.isRequired,
|
||||
colors: PropTypes.array.isRequired,
|
||||
labels: PropTypes.array.isRequired
|
||||
};
|
||||
|
||||
constructor(props) {
|
||||
@@ -26,54 +27,6 @@ class GenericDoughnutChart extends Component {
|
||||
this.chartRef = React.createRef();
|
||||
}
|
||||
|
||||
updateChart = () => {
|
||||
this.updateHandler = makeCancelable(this.props.apiCall(), {
|
||||
repeat: this.updateChart,
|
||||
interval: 10 * 60 * 1000
|
||||
});
|
||||
this.updateHandler.promise
|
||||
.then(apiData => {
|
||||
const colors = [
|
||||
"#20a8d8",
|
||||
"#f86c6b",
|
||||
"#4dbd74",
|
||||
"#f8cb00",
|
||||
"#263238",
|
||||
"#63c2de",
|
||||
"#b0bec5"
|
||||
];
|
||||
const data = [];
|
||||
const labels = [];
|
||||
const usedColors = [];
|
||||
|
||||
// Fill in dataset metadata
|
||||
let i = 0;
|
||||
for (let entry of apiData) {
|
||||
data.push(entry.percent);
|
||||
labels.push(entry.name.length !== 0 ? entry.name : entry.ip);
|
||||
usedColors.push(
|
||||
// If we ran out of colors, make a random one
|
||||
i < colors.length
|
||||
? colors[i]
|
||||
: "#" +
|
||||
parseInt("" + Math.random() * 0xffffff, 10)
|
||||
.toString(16)
|
||||
.padStart(6, "0")
|
||||
);
|
||||
|
||||
i++;
|
||||
}
|
||||
|
||||
this.setState({
|
||||
loading: false,
|
||||
data,
|
||||
colors: usedColors,
|
||||
labels
|
||||
});
|
||||
})
|
||||
.catch(ignoreCancel);
|
||||
};
|
||||
|
||||
handleClick = (e, index) => {
|
||||
// Hide the entry by clicking on the internal legend item
|
||||
const chart = this.chartRef.current.chartInstance;
|
||||
@@ -87,14 +40,6 @@ class GenericDoughnutChart extends Component {
|
||||
this.forceUpdate();
|
||||
};
|
||||
|
||||
componentDidMount() {
|
||||
this.updateChart();
|
||||
}
|
||||
|
||||
componentWillUnmount() {
|
||||
this.updateHandler.cancel();
|
||||
}
|
||||
|
||||
render() {
|
||||
const options = {
|
||||
legend: { display: false },
|
||||
@@ -119,7 +64,7 @@ class GenericDoughnutChart extends Component {
|
||||
const meta =
|
||||
this.chartRef.current !== null
|
||||
? this.chartRef.current.chartInstance.getDatasetMeta(0).data
|
||||
: this.state.data.map(() => ({ hidden: false }));
|
||||
: this.props.data.map(() => ({ hidden: false }));
|
||||
|
||||
return (
|
||||
<div className="card">
|
||||
@@ -134,19 +79,19 @@ class GenericDoughnutChart extends Component {
|
||||
data={{
|
||||
datasets: [
|
||||
{
|
||||
data: this.state.data,
|
||||
backgroundColor: this.state.colors
|
||||
data: this.props.data,
|
||||
backgroundColor: this.props.colors
|
||||
}
|
||||
],
|
||||
labels: this.state.labels
|
||||
labels: this.props.labels
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
<div className="float-right" style={{ width: "33%" }}>
|
||||
<ul className="chart-legend">
|
||||
{this.state.labels
|
||||
{this.props.labels
|
||||
// Zip label and color together
|
||||
.map((label, i) => [label, this.state.colors[i]])
|
||||
.map((label, i) => [label, this.props.colors[i]])
|
||||
// Create the list items
|
||||
.map(([label, color], i) => (
|
||||
<li
|
||||
@@ -161,7 +106,7 @@ class GenericDoughnutChart extends Component {
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
{this.state.loading ? (
|
||||
{this.props.loading ? (
|
||||
<div
|
||||
className="card-img-overlay"
|
||||
style={{ background: "rgba(255,255,255,0.7)" }}
|
||||
@@ -182,9 +127,76 @@ class GenericDoughnutChart extends Component {
|
||||
}
|
||||
}
|
||||
|
||||
GenericDoughnutChart.propTypes = {
|
||||
title: PropTypes.string.isRequired,
|
||||
apiCall: PropTypes.func.isRequired
|
||||
/**
|
||||
* Transform the API data into props for GenericDoughnutChart
|
||||
*
|
||||
* @param apiData the API data
|
||||
* @returns {{loading: boolean, data: Array, colors: Array, labels: Array}}
|
||||
* GenericDoughnutChart props
|
||||
*/
|
||||
export const transformData = apiData => {
|
||||
const colors = [
|
||||
"#20a8d8",
|
||||
"#f86c6b",
|
||||
"#4dbd74",
|
||||
"#f8cb00",
|
||||
"#263238",
|
||||
"#63c2de",
|
||||
"#b0bec5"
|
||||
];
|
||||
const data = [];
|
||||
const labels = [];
|
||||
const usedColors = [];
|
||||
|
||||
// Fill in dataset metadata
|
||||
let i = 0;
|
||||
for (let entry of apiData) {
|
||||
data.push(entry.percent);
|
||||
labels.push(entry.name.length !== 0 ? entry.name : entry.ip);
|
||||
usedColors.push(
|
||||
// If we ran out of colors, make a random one
|
||||
i < colors.length
|
||||
? colors[i]
|
||||
: "#" +
|
||||
parseInt("" + Math.random() * 0xffffff, 10)
|
||||
.toString(16)
|
||||
.padStart(6, "0")
|
||||
);
|
||||
|
||||
i++;
|
||||
}
|
||||
|
||||
return {
|
||||
loading: false,
|
||||
data,
|
||||
colors: usedColors,
|
||||
labels
|
||||
};
|
||||
};
|
||||
|
||||
export default GenericDoughnutChart;
|
||||
/**
|
||||
* The props used to show a loading state (either initial load or error)
|
||||
*
|
||||
* @returns {*} the loading props
|
||||
*/
|
||||
export const loadingProps = () => ({
|
||||
loading: true,
|
||||
data: [],
|
||||
colors: [],
|
||||
labels: []
|
||||
});
|
||||
|
||||
export default ({ apiCall, title, ...props }) => (
|
||||
<WithAPIData
|
||||
apiCall={apiCall}
|
||||
renderInitial={() => (
|
||||
<GenericDoughnutChart title={title} {...loadingProps()} {...props} />
|
||||
)}
|
||||
renderOk={data => (
|
||||
<GenericDoughnutChart title={title} {...transformData(data)} {...props} />
|
||||
)}
|
||||
renderErr={() => (
|
||||
<GenericDoughnutChart title={title} {...loadingProps()} {...props} />
|
||||
)}
|
||||
/>
|
||||
);
|
||||
|
||||
@@ -10,7 +10,11 @@
|
||||
|
||||
import React from "react";
|
||||
import { shallow } from "enzyme";
|
||||
import GenericDoughnutChart from "./GenericDoughnutChart";
|
||||
import {
|
||||
GenericDoughnutChart,
|
||||
transformData,
|
||||
loadingProps
|
||||
} from "./GenericDoughnutChart";
|
||||
|
||||
const fakeData = [
|
||||
{ name: "roberta.net", ip: "8.239.48.32", percent: 0.38411761010240625 },
|
||||
@@ -18,59 +22,36 @@ const fakeData = [
|
||||
{ name: "christop.net", ip: "181.219.42.222", percent: 0.6249293208519193 }
|
||||
];
|
||||
|
||||
it("shows loading indicator before first load", () => {
|
||||
it("shows loading indicator correctly", () => {
|
||||
const wrapper = shallow(
|
||||
<GenericDoughnutChart title={""} apiCall={ignoreAPI} />
|
||||
<GenericDoughnutChart title={""} {...loadingProps()} />
|
||||
);
|
||||
|
||||
expect(wrapper.state().loading).toBeTruthy();
|
||||
expect(wrapper.children(".card-img-overlay")).toExist();
|
||||
});
|
||||
|
||||
it("hides loading indicator after first load", async () => {
|
||||
it("hides loading indicator correctly", async () => {
|
||||
const wrapper = shallow(
|
||||
<GenericDoughnutChart
|
||||
title={""}
|
||||
apiCall={() => Promise.resolve(fakeData)}
|
||||
/>
|
||||
<GenericDoughnutChart title={""} {...loadingProps()} loading={false} />
|
||||
);
|
||||
|
||||
await tick();
|
||||
wrapper.update();
|
||||
|
||||
expect(wrapper.state().loading).toBeFalsy();
|
||||
expect(wrapper.children(".card-img-overlay")).not.toExist();
|
||||
});
|
||||
|
||||
it("loads API data correctly", async () => {
|
||||
const wrapper = shallow(
|
||||
<GenericDoughnutChart
|
||||
title={""}
|
||||
apiCall={() => Promise.resolve(fakeData)}
|
||||
/>
|
||||
);
|
||||
const data = transformData(fakeData);
|
||||
|
||||
await tick();
|
||||
wrapper.update();
|
||||
|
||||
expect(wrapper.state().colors).toEqual(["#20a8d8", "#f86c6b", "#4dbd74"]);
|
||||
expect(wrapper.state().labels[0]).toEqual(fakeData[0].name);
|
||||
expect(wrapper.state().labels[1]).toEqual(fakeData[1].ip);
|
||||
expect(wrapper.state().data).toEqual(fakeData.map(entry => entry.percent));
|
||||
expect(data.colors).toEqual(["#20a8d8", "#f86c6b", "#4dbd74"]);
|
||||
expect(data.labels[0]).toEqual(fakeData[0].name);
|
||||
expect(data.labels[1]).toEqual(fakeData[1].ip);
|
||||
expect(data.data).toEqual(fakeData.map(entry => entry.percent));
|
||||
});
|
||||
|
||||
it("displays the title", () => {
|
||||
const title = "title";
|
||||
const wrapper = shallow(
|
||||
<GenericDoughnutChart title={title} apiCall={ignoreAPI} />
|
||||
<GenericDoughnutChart title={title} {...loadingProps()} />
|
||||
);
|
||||
|
||||
expect(wrapper.find(".card-header")).toHaveText(title);
|
||||
});
|
||||
|
||||
it("calls the API callback", () => {
|
||||
const apiCall = jest.fn(ignoreAPI);
|
||||
shallow(<GenericDoughnutChart title={""} apiCall={apiCall} />);
|
||||
|
||||
expect(apiCall).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user