diff --git a/src/components/ForwardDestinationsChart.js b/src/components/ForwardDestinationsChart.js
index d4351f3..f1f9198 100644
--- a/src/components/ForwardDestinationsChart.js
+++ b/src/components/ForwardDestinationsChart.js
@@ -8,157 +8,15 @@
* This file is copyright under the latest version of the EUPL.
* Please see LICENSE file for your rights under this license. */
-import React, { Component } from 'react';
-import { Doughnut } from 'react-chartjs-2';
-import { translate } from 'react-i18next';
-import { makeCancelable, api, ignoreCancel } from "../utils";
+import React from "react";
+import { translate } from "react-i18next";
+import { api } from "../utils";
+import GenericDoughnutChart from "./GenericDoughnutChart";
-class ForwardDestinationsChart extends Component {
- state = {
- loading: true,
- data: [],
- colors: [],
- labels: []
- };
-
- constructor(props) {
- super(props);
- this.chartRef = React.createRef();
- }
-
- updateChart = () => {
- this.updateHandler = makeCancelable(
- api.getForwardDestinations(),
- { repeat: this.updateChart, interval: 10 * 60 * 1000 }
- );
- this.updateHandler.promise.then(forwardDestinations => {
- const colors = [
- "#20a8d8",
- "#f86c6b",
- "#4dbd74",
- "#f8cb00",
- "#263238",
- "#63c2de",
- "#b0bec5"
- ];
- const data = [];
- const labels = [];
- const usedColors = [];
-
- // Fill in dataset metadata
- let i = 0;
- for(let destination of forwardDestinations) {
- data.push(destination.percent);
- labels.push(destination.name.length !== 0 ? destination.name : destination.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 destination by clicking on the internal legend item
- const chart = this.chartRef.current.chartInstance;
- chart.legend.options.onClick.call(chart, e, chart.legend.legendItems[index]);
-
- // Cause an update so the external legend gets updated
- this.forceUpdate();
- };
-
- componentDidMount() {
- this.updateChart();
- }
-
- componentWillUnmount() {
- this.updateHandler.cancel();
- }
-
- render() {
- const { t } = this.props;
-
- const options = {
- legend: { display: false },
- tooltips: {
- enabled: true,
- callbacks: {
- title: () => t("Queries Answered By Destination"),
- label: (tooltipItems, data) => {
- const dataset = data.datasets[tooltipItems.datasetIndex];
- const label = data.labels[tooltipItems.index];
- return label + ": " + dataset.data[tooltipItems.index].toFixed(1) + "%";
- }
- }
- },
- maintainAspectRatio: false
- };
-
- // Get the metadata for the items, so we know if they are hidden or not.
- // If the chart has not been created yet, make some fake metadata.
- const meta = this.chartRef.current !== null
- ? this.chartRef.current.chartInstance.getDatasetMeta(0).data
- : this.state.data.map(() => ({ hidden: false }));
-
- return (
-
-
- {t("Queries Answered By")}
-
-
-
-
-
-
-
- {
- this.state.labels
- // Zip label and color together
- .map((label, i) => [label, this.state.colors[i]])
- // Create the list items
- .map(([label, color], i) => (
- - this.handleClick(e, i)}>
-
- {label}
-
- ))
- }
-
-
-
- {
- this.state.loading
- ?
-
-
-
- :
- null
- }
-
- );
- }
-}
+const ForwardDestinationsChart = ({ t }) => (
+
+);
export default translate("dashboard")(ForwardDestinationsChart);
diff --git a/src/components/GenericDoughnutChart.js b/src/components/GenericDoughnutChart.js
new file mode 100644
index 0000000..e18b4de
--- /dev/null
+++ b/src/components/GenericDoughnutChart.js
@@ -0,0 +1,167 @@
+/* Pi-hole: A black hole for Internet advertisements
+* (c) 2017 Pi-hole, LLC (https://pi-hole.net)
+* Network-wide ad blocking via your own hardware.
+*
+* Web Interface
+* Generic Doughnut Chart component
+*
+* This file is copyright under the latest version of the EUPL.
+* Please see LICENSE file for your rights under this license. */
+
+import React, { Component } from "react";
+import { Doughnut } from "react-chartjs-2";
+import PropTypes from "prop-types";
+import { makeCancelable, ignoreCancel } from "../utils";
+
+class GenericDoughnutChart extends Component {
+ state = {
+ loading: true,
+ data: [],
+ colors: [],
+ labels: []
+ };
+
+ constructor(props) {
+ super(props);
+ 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;
+ chart.legend.options.onClick.call(chart, e, chart.legend.legendItems[index]);
+
+ // Cause an update so the external legend gets updated
+ this.forceUpdate();
+ };
+
+ componentDidMount() {
+ this.updateChart();
+ }
+
+ componentWillUnmount() {
+ this.updateHandler.cancel();
+ }
+
+ render() {
+ const options = {
+ legend: { display: false },
+ tooltips: {
+ enabled: true,
+ callbacks: {
+ title: () => this.props.title,
+ label: (tooltipItems, data) => {
+ const dataset = data.datasets[tooltipItems.datasetIndex];
+ const label = data.labels[tooltipItems.index];
+ return label + ": " + dataset.data[tooltipItems.index].toFixed(1) + "%";
+ }
+ }
+ },
+ maintainAspectRatio: false
+ };
+
+ // Get the metadata for the items, so we know if they are hidden or not.
+ // If the chart has not been created yet, make some fake metadata.
+ const meta = this.chartRef.current !== null
+ ? this.chartRef.current.chartInstance.getDatasetMeta(0).data
+ : this.state.data.map(() => ({ hidden: false }));
+
+ return (
+
+
+ {this.props.title}
+
+
+
+
+
+
+
+ {
+ this.state.labels
+ // Zip label and color together
+ .map((label, i) => [label, this.state.colors[i]])
+ // Create the list items
+ .map(([label, color], i) => (
+ - this.handleClick(e, i)}>
+
+ {label}
+
+ ))
+ }
+
+
+
+ {
+ this.state.loading
+ ?
+
+
+
+ :
+ null
+ }
+
+ );
+ }
+}
+
+GenericDoughnutChart.propTypes = {
+ title: PropTypes.string.isRequired,
+ apiCall: PropTypes.func.isRequired
+};
+
+export default GenericDoughnutChart;
\ No newline at end of file
diff --git a/src/components/QueryTypesChart.js b/src/components/QueryTypesChart.js
index c1016ec..7a41536 100644
--- a/src/components/QueryTypesChart.js
+++ b/src/components/QueryTypesChart.js
@@ -8,154 +8,15 @@
* This file is copyright under the latest version of the EUPL.
* Please see LICENSE file for your rights under this license. */
-import React, { Component } from 'react';
-import { Doughnut } from 'react-chartjs-2';
+import React from 'react';
import { translate } from 'react-i18next';
-import { makeCancelable, api, ignoreCancel } from "../utils";
+import { api } from "../utils";
+import GenericDoughnutChart from "./GenericDoughnutChart";
-class QueryTypesChart extends Component {
- state = {
- loading: true,
- data: [],
- colors: [],
- labels: []
- };
-
- constructor(props) {
- super(props);
- this.chartRef = React.createRef();
- }
-
- updateChart = () => {
- this.updateHandler = makeCancelable(api.getQueryTypes(), { repeat: this.updateChart, interval: 10 * 60 * 1000 });
- this.updateHandler.promise.then(queryTypes => {
- const colors = [
- "#20a8d8",
- "#f86c6b",
- "#4dbd74",
- "#f8cb00",
- "#263238",
- "#63c2de",
- "#b0bec5"
- ];
- const data = [];
- const labels = [];
- const usedColors = [];
-
- // Fill in dataset metadata
- let i = 0;
- for(let queryType of queryTypes) {
- data.push(queryType.percent);
- labels.push(queryType.name);
- usedColors.push(
- // If we ran out of colors, make a random one
- i < colors.length
- ? colors[i]
- : '#' + parseInt("" + Math.random() * 0xffffff, 10).toString(16)
- );
-
- i++;
- }
-
- this.setState({
- loading: false,
- data,
- colors: usedColors,
- labels
- })
- }).catch(ignoreCancel);
- };
-
- handleClick = (e, index) => {
- // Hide the destination by clicking on the internal legend item
- const chart = this.chartRef.current.chartInstance;
- chart.legend.options.onClick.call(chart, e, chart.legend.legendItems[index]);
-
- // Cause an update so the external legend gets updated
- this.forceUpdate();
- };
-
- componentDidMount() {
- this.updateChart();
- }
-
- componentWillUnmount() {
- this.updateHandler.cancel();
- }
-
- render() {
- const { t } = this.props;
-
- const options = {
- legend: { display: false },
- tooltips: {
- enabled: true,
- callbacks: {
- title: () => t("Query Types"),
- label: (tooltipItems, data) => {
- const dataset = data.datasets[tooltipItems.datasetIndex];
- const label = data.labels[tooltipItems.index];
- return label + ": " + dataset.data[tooltipItems.index].toFixed(1) + "%";
- }
- }
- },
- maintainAspectRatio: false
- };
-
- // Get the metadata for the items, so we know if they are hidden or not.
- // If the chart has not been created yet, make some fake metadata.
- const meta = this.chartRef.current !== null
- ? this.chartRef.current.chartInstance.getDatasetMeta(0).data
- : this.state.data.map(() => ({ hidden: false }));
-
- return (
-
-
- {t("Query Types")}
-
-
-
-
-
-
-
- {
- this.state.labels
- // Zip label and color together
- .map((label, i) => [label, this.state.colors[i]])
- // Create the list items
- .map(([label, color], i) => (
- - this.handleClick(e, i)}>
-
- {label}
-
- ))
- }
-
-
-
- {
- this.state.loading
- ?
-
-
-
- :
- null
- }
-
- );
- }
-}
+const QueryTypesChart = ({ t }) => (
+
+);
export default translate("dashboard")(QueryTypesChart);