mirror of
https://github.com/gosticks/web.git
synced 2026-10-06 14:47:32 +00:00
Add WithAPIData component
This component acts as a provider of API data to decouple the view from the data source. Previously the components would handle getting the API data themselves, which hurt testing. util.js has been split out into three files in a new "util" directory. Signed-off-by: Mcat12 <newtoncat12@yahoo.com>
This commit is contained in:
@@ -14,7 +14,7 @@ import { NavLink } from "react-router-dom";
|
||||
import { Nav, NavItem } from "reactstrap";
|
||||
import logo from "../../img/logo.svg";
|
||||
import { mobileSidebarHide } from "./Header";
|
||||
import { api } from "../../utils";
|
||||
import api from "../../util/api";
|
||||
import StatusBadge from "./StatusBadge";
|
||||
|
||||
const handleClick = e => {
|
||||
|
||||
@@ -11,7 +11,7 @@
|
||||
import React from "react";
|
||||
import { shallow } from "enzyme";
|
||||
import { dropDownClassList, navDropdown, navItem, navList } from "./Sidebar";
|
||||
import { api } from "../../utils";
|
||||
import api from "../../util/api";
|
||||
|
||||
it("expands active drop down items", () => {
|
||||
const classList = dropDownClassList("/testRoute", {
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import React, { Component } from "react";
|
||||
import { translate } from "react-i18next";
|
||||
import { api, makeCancelable, ignoreCancel } from "../../utils";
|
||||
import api from "../../util/api";
|
||||
import { makeCancelable, ignoreCancel } from "../../util";
|
||||
|
||||
class StatusBadge extends Component {
|
||||
state = {
|
||||
|
||||
@@ -0,0 +1,102 @@
|
||||
/* 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
|
||||
* WithAPIData Component
|
||||
*
|
||||
* This file is copyright under the latest version of the EUPL.
|
||||
* Please see LICENSE file for your rights under this license. */
|
||||
|
||||
import { Component } from "react";
|
||||
import PropTypes from "prop-types";
|
||||
import { ignoreCancel, makeCancelable, Result } from "../../util";
|
||||
|
||||
/**
|
||||
* A component for getting API data. It can retrieve information once,
|
||||
* periodically, or on call (refresh).
|
||||
*/
|
||||
export class WithAPIData extends Component {
|
||||
static propTypes = {
|
||||
apiCall: PropTypes.func.isRequired,
|
||||
repeatOptions: PropTypes.shape({
|
||||
ignoreCancel: PropTypes.bool,
|
||||
interval: PropTypes.number
|
||||
}),
|
||||
/**
|
||||
* Render the children before the first API request is done
|
||||
*/
|
||||
renderInitial: PropTypes.func.isRequired,
|
||||
/**
|
||||
* Render the children if the API request succeeded
|
||||
*
|
||||
* @param data the data returned from the API call
|
||||
* @param refresh a function to trigger an asynchronous data refresh
|
||||
*/
|
||||
renderOk: PropTypes.func.isRequired,
|
||||
/**
|
||||
* Render the children if the API request failed
|
||||
*
|
||||
* @param error the error returned from the API call
|
||||
* @param refresh a function to trigger an asynchronous data refresh
|
||||
*/
|
||||
renderErr: PropTypes.func.isRequired
|
||||
};
|
||||
|
||||
static defaultProps = {
|
||||
repeatOptions: {
|
||||
ignoreCancel: true,
|
||||
repeatInterval: 0
|
||||
}
|
||||
};
|
||||
|
||||
state = {
|
||||
apiResult: null
|
||||
};
|
||||
|
||||
loadData = () => {
|
||||
// Only repeat if there is a non-zero repeat interval
|
||||
const cancelOptions = {
|
||||
repeat: this.props.repeatOptions.interval !== 0 ? this.loadData : null,
|
||||
interval: this.props.repeatOptions.interval
|
||||
};
|
||||
|
||||
this.dataHandle = makeCancelable(this.props.apiCall(), cancelOptions);
|
||||
|
||||
if (this.props.repeatOptions.ignoreCancel) {
|
||||
this.dataHandle.promise = this.dataHandle.promise.catch(ignoreCancel);
|
||||
}
|
||||
|
||||
this.dataHandle.promise
|
||||
.then(data => {
|
||||
this.setState({
|
||||
apiResult: Result.Ok(data)
|
||||
});
|
||||
})
|
||||
.catch(error => {
|
||||
this.setState({
|
||||
apiResult: Result.Err(error)
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
componentDidMount() {
|
||||
this.loadData();
|
||||
}
|
||||
|
||||
componentWillUnmount() {
|
||||
this.dataHandle.cancel();
|
||||
}
|
||||
|
||||
render() {
|
||||
if (!this.state.apiResult) {
|
||||
return this.props.renderInitial();
|
||||
}
|
||||
|
||||
if (Result.isOk(this.state.apiResult)) {
|
||||
return this.props.renderOk(this.state.apiResult.value, this.loadData);
|
||||
} else {
|
||||
return this.props.renderErr(this.state.apiResult.value, this.loadData);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -12,7 +12,8 @@ import React, { Component } from "react";
|
||||
import ReactDOM from "react-dom";
|
||||
import { Line } from "react-chartjs-2";
|
||||
import { translate } from "react-i18next";
|
||||
import { padNumber, api, makeCancelable, ignoreCancel } from "../../utils";
|
||||
import { padNumber, makeCancelable, ignoreCancel } from "../../util";
|
||||
import api from "../../util/api";
|
||||
import ChartTooltip from "./ChartTooltip";
|
||||
|
||||
class ClientsGraph extends Component {
|
||||
|
||||
@@ -10,7 +10,7 @@
|
||||
|
||||
import React from "react";
|
||||
import { translate } from "react-i18next";
|
||||
import { api } from "../../utils";
|
||||
import api from "../../util/api";
|
||||
import GenericDoughnutChart from "./GenericDoughnutChart";
|
||||
|
||||
const ForwardDestinationsChart = ({ t }) => (
|
||||
|
||||
@@ -11,7 +11,7 @@
|
||||
import React, { Component } from "react";
|
||||
import { Doughnut } from "react-chartjs-2";
|
||||
import PropTypes from "prop-types";
|
||||
import { makeCancelable, ignoreCancel } from "../../utils";
|
||||
import { makeCancelable, ignoreCancel } from "../../util";
|
||||
|
||||
class GenericDoughnutChart extends Component {
|
||||
state = {
|
||||
|
||||
@@ -11,7 +11,8 @@
|
||||
import React, { Component } from "react";
|
||||
import { Line } from "react-chartjs-2";
|
||||
import { translate } from "react-i18next";
|
||||
import { padNumber, api, makeCancelable, ignoreCancel } from "../../utils";
|
||||
import { padNumber, makeCancelable, ignoreCancel } from "../../util";
|
||||
import api from "../../util/api";
|
||||
|
||||
class QueriesGraph extends Component {
|
||||
state = {
|
||||
|
||||
@@ -10,7 +10,7 @@
|
||||
|
||||
import React from "react";
|
||||
import { translate } from "react-i18next";
|
||||
import { api } from "../../utils";
|
||||
import api from "../../util/api";
|
||||
import GenericDoughnutChart from "./GenericDoughnutChart";
|
||||
|
||||
const QueryTypesChart = ({ t }) => (
|
||||
|
||||
@@ -9,57 +9,21 @@
|
||||
* Please see LICENSE file for your rights under this license. */
|
||||
|
||||
import React, { Component, Fragment } from "react";
|
||||
import PropTypes from "prop-types";
|
||||
import { translate } from "react-i18next";
|
||||
import { api, ignoreCancel, makeCancelable } from "../../utils";
|
||||
import { WithAPIData } from "../common/WithAPIData";
|
||||
import api from "../../util/api";
|
||||
|
||||
class SummaryStats extends Component {
|
||||
state = {
|
||||
blockedQueries: "---",
|
||||
totalQueries: "---",
|
||||
percentBlocked: "---",
|
||||
gravityDomains: "---",
|
||||
uniqueClients: "---"
|
||||
static propTypes = {
|
||||
totalQueries: PropTypes.string.isRequired,
|
||||
blockedQueries: PropTypes.string.isRequired,
|
||||
percentBlocked: PropTypes.string.isRequired,
|
||||
gravityDomains: PropTypes.string.isRequired,
|
||||
uniqueClients: PropTypes.oneOfType([PropTypes.number, PropTypes.string])
|
||||
.isRequired
|
||||
};
|
||||
|
||||
constructor(props) {
|
||||
super(props);
|
||||
this.updateStats = this.updateStats.bind(this);
|
||||
}
|
||||
|
||||
updateStats() {
|
||||
this.updateHandler = makeCancelable(api.getSummary(), {
|
||||
repeat: this.updateStats,
|
||||
interval: 5000
|
||||
});
|
||||
this.updateHandler.promise
|
||||
.then(res => {
|
||||
this.setState({
|
||||
blockedQueries: res.blocked_queries.toLocaleString(),
|
||||
totalQueries: res.total_queries.toLocaleString(),
|
||||
percentBlocked: res.percent_blocked.toFixed(2).toLocaleString() + "%",
|
||||
gravityDomains: res.domains_blocked.toLocaleString(),
|
||||
uniqueClients: res.unique_clients
|
||||
});
|
||||
})
|
||||
.catch(ignoreCancel)
|
||||
.catch(() => {
|
||||
this.setState({
|
||||
totalQueries: "Lost",
|
||||
blockedQueries: "Connection",
|
||||
percentBlocked: "To",
|
||||
gravityDomains: "API"
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
this.updateStats();
|
||||
}
|
||||
|
||||
componentWillUnmount() {
|
||||
this.updateHandler.cancel();
|
||||
}
|
||||
|
||||
render() {
|
||||
const { t } = this.props;
|
||||
|
||||
@@ -73,10 +37,10 @@ class SummaryStats extends Component {
|
||||
</div>
|
||||
</div>
|
||||
<div className="card-img-overlay">
|
||||
<h3>{this.state.totalQueries}</h3>
|
||||
<h3>{this.props.totalQueries}</h3>
|
||||
<p style={{ marginBottom: "0px" }}>
|
||||
{t("Total Queries ({{count}} clients)", {
|
||||
count: this.state.uniqueClients
|
||||
count: this.props.uniqueClients
|
||||
})}
|
||||
</p>
|
||||
</div>
|
||||
@@ -90,7 +54,7 @@ class SummaryStats extends Component {
|
||||
</div>
|
||||
</div>
|
||||
<div className="card-img-overlay">
|
||||
<h3>{this.state.blockedQueries}</h3>
|
||||
<h3>{this.props.blockedQueries}</h3>
|
||||
<p style={{ marginBottom: "0px" }}>{t("Queries Blocked")}</p>
|
||||
</div>
|
||||
</div>
|
||||
@@ -103,7 +67,7 @@ class SummaryStats extends Component {
|
||||
</div>
|
||||
</div>
|
||||
<div className="card-img-overlay">
|
||||
<h3>{this.state.percentBlocked}</h3>
|
||||
<h3>{this.props.percentBlocked}</h3>
|
||||
<p style={{ marginBottom: "0px" }}>{t("Percent Blocked")}</p>
|
||||
</div>
|
||||
</div>
|
||||
@@ -116,7 +80,7 @@ class SummaryStats extends Component {
|
||||
</div>
|
||||
</div>
|
||||
<div className="card-img-overlay">
|
||||
<h3>{this.state.gravityDomains}</h3>
|
||||
<h3>{this.props.gravityDomains}</h3>
|
||||
<p style={{ marginBottom: "0px" }}>{t("Domains On Blocklist")}</p>
|
||||
</div>
|
||||
</div>
|
||||
@@ -126,4 +90,55 @@ class SummaryStats extends Component {
|
||||
}
|
||||
}
|
||||
|
||||
export default translate(["common", "dashboard"])(SummaryStats);
|
||||
/**
|
||||
* Transform the API data into props for the component
|
||||
*
|
||||
* @param data the API data
|
||||
* @returns {*} the transformed props
|
||||
*/
|
||||
export const transformData = data => ({
|
||||
totalQueries: data.total_queries.toLocaleString(),
|
||||
blockedQueries: data.blocked_queries.toLocaleString(),
|
||||
percentBlocked: data.percent_blocked.toFixed(2).toLocaleString() + "%",
|
||||
gravityDomains: data.domains_blocked.toLocaleString(),
|
||||
uniqueClients: data.unique_clients
|
||||
});
|
||||
|
||||
/**
|
||||
* The props the summary stats should use when it fails to get the API data
|
||||
* (it does not need the error object)
|
||||
*
|
||||
* @returns {*} the error props
|
||||
*/
|
||||
export const errorState = () => ({
|
||||
totalQueries: "Lost",
|
||||
blockedQueries: "Connection",
|
||||
percentBlocked: "To",
|
||||
gravityDomains: "API",
|
||||
uniqueClients: ""
|
||||
});
|
||||
|
||||
export const TranslatedSummaryStats = translate(["common", "dashboard"])(
|
||||
SummaryStats
|
||||
);
|
||||
|
||||
export default props => (
|
||||
<WithAPIData
|
||||
apiCall={api.getSummary}
|
||||
repeatOptions={{ interval: 5000 }}
|
||||
renderInitial={() => (
|
||||
<TranslatedSummaryStats
|
||||
blockedQueries="---"
|
||||
totalQueries="---"
|
||||
percentBlocked="---"
|
||||
gravityDomains="---"
|
||||
uniqueClients="---"
|
||||
{...props}
|
||||
/>
|
||||
)}
|
||||
renderOk={data => (
|
||||
<TranslatedSummaryStats {...transformData(data)} {...props} />
|
||||
)}
|
||||
renderErr={() => <TranslatedSummaryStats {...errorState()} {...props} />}
|
||||
/>
|
||||
);
|
||||
|
||||
@@ -11,7 +11,11 @@
|
||||
import React from "react";
|
||||
import { shallow } from "enzyme";
|
||||
import fetchMock from "fetch-mock";
|
||||
import SummaryStats from "./SummaryStats";
|
||||
import {
|
||||
TranslatedSummaryStats,
|
||||
transformData,
|
||||
errorState
|
||||
} from "./SummaryStats";
|
||||
|
||||
const endpoint = "/admin/api/stats/summary";
|
||||
const fakeData = {
|
||||
@@ -30,7 +34,9 @@ const fakeData = {
|
||||
it("displays summary stats correctly", async () => {
|
||||
fetchMock.mock(endpoint, fakeData);
|
||||
|
||||
const wrapper = shallow(<SummaryStats />);
|
||||
const wrapper = shallow(
|
||||
<TranslatedSummaryStats {...transformData(fakeData)} />
|
||||
);
|
||||
|
||||
await tick();
|
||||
wrapper.update();
|
||||
@@ -38,7 +44,6 @@ it("displays summary stats correctly", async () => {
|
||||
expect(wrapper.childAt(0).find("h3")).toHaveText(
|
||||
fakeData.total_queries.toLocaleString()
|
||||
);
|
||||
expect(wrapper.state().uniqueClients).toEqual(fakeData.unique_clients);
|
||||
expect(wrapper.childAt(1).find("h3")).toHaveText(
|
||||
fakeData.blocked_queries.toLocaleString()
|
||||
);
|
||||
@@ -53,7 +58,7 @@ it("displays summary stats correctly", async () => {
|
||||
it("displays an error message on error", async () => {
|
||||
fetchMock.mock(endpoint, { error: {} });
|
||||
|
||||
const wrapper = shallow(<SummaryStats />);
|
||||
const wrapper = shallow(<TranslatedSummaryStats {...errorState()} />);
|
||||
|
||||
await tick();
|
||||
wrapper.update();
|
||||
|
||||
@@ -10,7 +10,7 @@
|
||||
|
||||
import React from "react";
|
||||
import { translate } from "react-i18next";
|
||||
import { api } from "../../utils";
|
||||
import api from "../../util/api";
|
||||
import TopTable from "./TopTable";
|
||||
|
||||
const TopBlocked = ({ t, ...props }) => (
|
||||
|
||||
@@ -10,7 +10,7 @@
|
||||
|
||||
import React from "react";
|
||||
import { translate } from "react-i18next";
|
||||
import { api } from "../../utils";
|
||||
import api from "../../util/api";
|
||||
import TopTable from "./TopTable";
|
||||
|
||||
const TopClients = ({ t, ...props }) => (
|
||||
|
||||
@@ -10,7 +10,7 @@
|
||||
|
||||
import React from "react";
|
||||
import { translate } from "react-i18next";
|
||||
import { api } from "../../utils";
|
||||
import api from "../../util/api";
|
||||
import TopTable from "./TopTable";
|
||||
|
||||
const TopDomains = ({ t, ...props }) => (
|
||||
|
||||
@@ -10,7 +10,7 @@
|
||||
|
||||
import React, { Component } from "react";
|
||||
import PropTypes from "prop-types";
|
||||
import { ignoreCancel, makeCancelable } from "../../utils";
|
||||
import { ignoreCancel, makeCancelable } from "../../util";
|
||||
|
||||
class TopTable extends Component {
|
||||
state = {
|
||||
|
||||
@@ -11,7 +11,7 @@
|
||||
import React, { Component } from "react";
|
||||
import PropTypes from "prop-types";
|
||||
import { translate } from "react-i18next";
|
||||
import { api } from "../../utils";
|
||||
import api from "../../util/api";
|
||||
|
||||
class DomainInput extends Component {
|
||||
state = {
|
||||
|
||||
@@ -11,7 +11,7 @@
|
||||
import React from "react";
|
||||
import { shallow } from "enzyme";
|
||||
import DomainInput from "./DomainInput";
|
||||
import { api } from "../../utils";
|
||||
import api from "../../util/api";
|
||||
import { isValidDomain } from "../../validate";
|
||||
|
||||
it("has a placeholder", () => {
|
||||
|
||||
@@ -11,7 +11,8 @@
|
||||
import React, { Component } from "react";
|
||||
import PropTypes from "prop-types";
|
||||
import { translate } from "react-i18next";
|
||||
import { api, ignoreCancel, makeCancelable } from "../../utils";
|
||||
import { ignoreCancel, makeCancelable } from "../../util";
|
||||
import api from "../../util/api";
|
||||
|
||||
class DomainList extends Component {
|
||||
onRemove(domain) {
|
||||
|
||||
@@ -11,7 +11,7 @@
|
||||
import React from "react";
|
||||
import { shallow } from "enzyme";
|
||||
import DomainList from "./DomainList";
|
||||
import { api } from "../../utils";
|
||||
import api from "../../util/api";
|
||||
|
||||
const domains = ["domain1.com", "domain2.com", "domain3.com"];
|
||||
|
||||
|
||||
@@ -14,7 +14,7 @@ import { translate } from "react-i18next";
|
||||
import DomainInput from "./DomainInput";
|
||||
import Alert from "../common/Alert";
|
||||
import DomainList from "./DomainList";
|
||||
import { ignoreCancel, makeCancelable } from "../../utils";
|
||||
import { ignoreCancel, makeCancelable } from "../../util";
|
||||
|
||||
class ListPage extends Component {
|
||||
state = {
|
||||
|
||||
@@ -11,7 +11,8 @@
|
||||
import React, { Component } from "react";
|
||||
import ReactTable from "react-table";
|
||||
import { translate } from "react-i18next";
|
||||
import { api, ignoreCancel, makeCancelable, padNumber } from "../../utils";
|
||||
import { ignoreCancel, makeCancelable, padNumber } from "../../util";
|
||||
import api from "../../util/api";
|
||||
import "react-table/react-table.css";
|
||||
|
||||
class QueryLog extends Component {
|
||||
|
||||
@@ -10,7 +10,8 @@
|
||||
|
||||
import React, { Component } from "react";
|
||||
import { translate } from "react-i18next";
|
||||
import { api, ignoreCancel, makeCancelable } from "../../utils";
|
||||
import { ignoreCancel, makeCancelable } from "../../util";
|
||||
import api from "../../util/api";
|
||||
import {
|
||||
Button,
|
||||
Col,
|
||||
|
||||
@@ -10,7 +10,8 @@
|
||||
|
||||
import React, { Component } from "react";
|
||||
import { translate } from "react-i18next";
|
||||
import { api, ignoreCancel, makeCancelable } from "../../utils";
|
||||
import { ignoreCancel, makeCancelable } from "../../util";
|
||||
import api from "../../util/api";
|
||||
|
||||
class DNSInfo extends Component {
|
||||
state = {
|
||||
|
||||
@@ -10,7 +10,8 @@
|
||||
|
||||
import React, { Component } from "react";
|
||||
import { translate } from "react-i18next";
|
||||
import { api, ignoreCancel, makeCancelable } from "../../utils";
|
||||
import { ignoreCancel, makeCancelable } from "../../util";
|
||||
import api from "../../util/api";
|
||||
|
||||
class FTLInfo extends Component {
|
||||
state = {
|
||||
|
||||
@@ -10,7 +10,8 @@
|
||||
|
||||
import React, { Component } from "react";
|
||||
import { translate } from "react-i18next";
|
||||
import { api, ignoreCancel, makeCancelable } from "../../utils";
|
||||
import { ignoreCancel, makeCancelable } from "../../util";
|
||||
import api from "../../util/api";
|
||||
import { Col, Form, FormGroup, Input, Label } from "reactstrap";
|
||||
|
||||
class NetworkInfo extends Component {
|
||||
|
||||
@@ -10,7 +10,8 @@
|
||||
|
||||
import React, { Component } from "react";
|
||||
import { translate } from "react-i18next";
|
||||
import { api, ignoreCancel, makeCancelable } from "../../utils";
|
||||
import { ignoreCancel, makeCancelable } from "../../util";
|
||||
import api from "../../util/api";
|
||||
import VersionCard from "./VersionCard";
|
||||
|
||||
class VersionInfo extends Component {
|
||||
|
||||
@@ -13,7 +13,7 @@ import { Switch, Route, Redirect } from "react-router-dom";
|
||||
import Header, { mobileSidebarHide } from "../components/common/Header";
|
||||
import Sidebar from "../components/common/Sidebar";
|
||||
import Footer from "../components/common/Footer";
|
||||
import { api } from "../utils";
|
||||
import api from "../util/api";
|
||||
import { nav } from "../routes";
|
||||
|
||||
export default props => (
|
||||
|
||||
+1
-1
@@ -16,7 +16,7 @@ import "font-awesome/css/font-awesome.min.css";
|
||||
import "simple-line-icons/css/simple-line-icons.css";
|
||||
import "./scss/style.css";
|
||||
import Full from "./containers/Full";
|
||||
import { api } from "./utils";
|
||||
import api from "./util/api";
|
||||
import { setupI18n } from "./i18n";
|
||||
|
||||
// Before rendering anything, check if there is a session cookie.
|
||||
|
||||
+1
-1
@@ -11,7 +11,7 @@
|
||||
import { configure } from "enzyme";
|
||||
import Adapter from "enzyme-adapter-react-16";
|
||||
import "jest-enzyme";
|
||||
import { api } from "./utils";
|
||||
import api from "./util/api";
|
||||
import fetchMock from "fetch-mock";
|
||||
|
||||
// Setup enzyme
|
||||
|
||||
@@ -0,0 +1,85 @@
|
||||
import http from "./http";
|
||||
|
||||
export default {
|
||||
loggedIn: false,
|
||||
authenticate(key) {
|
||||
return http.get("auth", {
|
||||
headers: new Headers({ "X-Pi-hole-Authenticate": key })
|
||||
});
|
||||
},
|
||||
logout() {
|
||||
return http.delete("auth");
|
||||
},
|
||||
getSummary() {
|
||||
return http.get("stats/summary");
|
||||
},
|
||||
getHistoryGraph() {
|
||||
return http.get("stats/overTime/history");
|
||||
},
|
||||
getClientsGraph() {
|
||||
return http.get("stats/overTime/clients");
|
||||
},
|
||||
getQueryTypes() {
|
||||
return http.get("stats/query_types");
|
||||
},
|
||||
getForwardDestinations() {
|
||||
return http.get("stats/forward_destinations");
|
||||
},
|
||||
getTopDomains() {
|
||||
return http.get("stats/top_domains");
|
||||
},
|
||||
getTopBlocked() {
|
||||
return http.get("stats/top_blocked");
|
||||
},
|
||||
getTopClients() {
|
||||
return http.get("stats/top_clients");
|
||||
},
|
||||
getHistory() {
|
||||
return http.get("stats/history");
|
||||
},
|
||||
getWhitelist() {
|
||||
return http.get("dns/whitelist");
|
||||
},
|
||||
getBlacklist() {
|
||||
return http.get("dns/blacklist");
|
||||
},
|
||||
getRegexlist() {
|
||||
return http.get("dns/regexlist");
|
||||
},
|
||||
addWhitelist(domain) {
|
||||
return http.post("dns/whitelist", { domain: domain });
|
||||
},
|
||||
addBlacklist(domain) {
|
||||
return http.post("dns/blacklist", { domain: domain });
|
||||
},
|
||||
addRegexlist(domain) {
|
||||
return http.post("dns/regexlist", { domain: domain });
|
||||
},
|
||||
removeWhitelist(domain) {
|
||||
return http.delete("dns/whitelist/" + domain);
|
||||
},
|
||||
removeBlacklist(domain) {
|
||||
return http.delete("dns/blacklist/" + domain);
|
||||
},
|
||||
removeRegexlist(domain) {
|
||||
return http.delete("dns/regexlist/" + encodeURIComponent(domain));
|
||||
},
|
||||
getStatus() {
|
||||
return http.get("dns/status");
|
||||
},
|
||||
getNetworkInfo() {
|
||||
return http.get("settings/network");
|
||||
},
|
||||
getVersion() {
|
||||
return http.get("version");
|
||||
},
|
||||
getFTLdb() {
|
||||
return http.get("settings/ftldb");
|
||||
},
|
||||
getDNSInfo() {
|
||||
return http.get("settings/dns");
|
||||
},
|
||||
getDHCPInfo() {
|
||||
return http.get("settings/dhcp");
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,172 @@
|
||||
/* 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
|
||||
* Basic HTTP functions for accessing API endpoints
|
||||
*
|
||||
* This file is copyright under the latest version of the EUPL.
|
||||
* Please see LICENSE file for your rights under this license. */
|
||||
|
||||
import api from "./api";
|
||||
import config from "../config";
|
||||
|
||||
/**
|
||||
* A group of HTTP functions. Each function parses the response checks for
|
||||
* errors
|
||||
*/
|
||||
export default {
|
||||
/**
|
||||
* Perform a GET request
|
||||
*
|
||||
* @param url the URL to access
|
||||
* @param options optional fetch configuration
|
||||
* @returns {Promise<* | never>} a promise with the data or error returned
|
||||
*/
|
||||
get(url, options = {}) {
|
||||
return fetch(urlFor(url), {
|
||||
credentials: credentialType(),
|
||||
...options
|
||||
})
|
||||
.then(checkIfLoggedOut)
|
||||
.then(convertJSON)
|
||||
.catch(convertJSON)
|
||||
.then(checkForErrors);
|
||||
},
|
||||
|
||||
/**
|
||||
* Perform a POST request
|
||||
*
|
||||
* @param url the URL to access
|
||||
* @param data the data to send
|
||||
* @returns {Promise<* | never>} a promise with the data or error returned
|
||||
*/
|
||||
post(url, data) {
|
||||
return fetch(urlFor(url), {
|
||||
method: "POST",
|
||||
body: JSON.stringify(data),
|
||||
headers: new Headers({ "Content-Type": "application/json" }),
|
||||
credentials: credentialType()
|
||||
})
|
||||
.then(checkIfLoggedOut)
|
||||
.then(convertJSON)
|
||||
.catch(convertJSON)
|
||||
.then(checkForErrors);
|
||||
},
|
||||
|
||||
/**
|
||||
* Perform a PUT request
|
||||
*
|
||||
* @param url the URL to access
|
||||
* @param data the data to send
|
||||
* @returns {Promise<* | never>} a promise with the data or error returned
|
||||
*/
|
||||
put(url, data) {
|
||||
return fetch(urlFor(url), {
|
||||
method: "PUT",
|
||||
body: JSON.stringify(data),
|
||||
headers: new Headers({ "Content-Type": "application/json" }),
|
||||
credentials: credentialType()
|
||||
})
|
||||
.then(checkIfLoggedOut)
|
||||
.then(convertJSON)
|
||||
.catch(convertJSON)
|
||||
.then(checkForErrors);
|
||||
},
|
||||
|
||||
/**
|
||||
* Perform a DELETE request
|
||||
*
|
||||
* @param url the URL to access
|
||||
* @returns {Promise<* | never>} a promise with the data or error returned
|
||||
*/
|
||||
delete(url) {
|
||||
return fetch(urlFor(url), {
|
||||
method: "DELETE",
|
||||
credentials: credentialType()
|
||||
})
|
||||
.then(checkIfLoggedOut)
|
||||
.then(convertJSON)
|
||||
.catch(convertJSON)
|
||||
.then(checkForErrors);
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* If the user is logged in, check if the user's session has lapsed.
|
||||
* If so, log them out and refresh the page.
|
||||
*
|
||||
* @param response the Response from fetch
|
||||
* @return {Promise} if logged in, the response, otherwise a canceled promise
|
||||
*/
|
||||
const checkIfLoggedOut = response => {
|
||||
if (api.loggedIn && response.status === 401) {
|
||||
// Clear the user's old session and refresh the page
|
||||
document.cookie =
|
||||
"user_id=; Path=/; Expires=Thu, 01 Jan 1970 00:00:01 GMT;";
|
||||
window.location.reload();
|
||||
return Promise.reject({ isCanceled: true });
|
||||
}
|
||||
|
||||
return Promise.resolve(response);
|
||||
};
|
||||
|
||||
/**
|
||||
* If the input is a Response, return a promise for parsing the JSON.
|
||||
* If the input is an Error, return a rejecting promise with error.
|
||||
* If the request was canceled, return a rejecting promise with cancel object.
|
||||
*
|
||||
* @param data a Response or Error
|
||||
* @returns {*} a promise with the parsed JSON, or the error
|
||||
*/
|
||||
const convertJSON = data => {
|
||||
if (data.isCanceled || data instanceof Error) {
|
||||
return Promise.reject(data);
|
||||
}
|
||||
|
||||
return data.json();
|
||||
};
|
||||
|
||||
/**
|
||||
* Check for an error returned by the API
|
||||
*
|
||||
* @param data the parsed JSON body of the response
|
||||
* @returns {*} a resolving promise with the data if no error, otherwise a
|
||||
* rejecting promise with the error
|
||||
*/
|
||||
const checkForErrors = data => {
|
||||
if (data.error) {
|
||||
return Promise.reject(data.error);
|
||||
}
|
||||
|
||||
return Promise.resolve(data);
|
||||
};
|
||||
|
||||
/**
|
||||
* Get the URL for an endpoint
|
||||
*
|
||||
* @param endpoint the endpoint
|
||||
* @returns {string} the URL for the endpoint
|
||||
*/
|
||||
const urlFor = endpoint => {
|
||||
let apiLocation;
|
||||
|
||||
if (config.fakeAPI) {
|
||||
apiLocation = process.env.PUBLIC_URL + "/fakeAPI";
|
||||
} else {
|
||||
apiLocation = "/admin/api";
|
||||
}
|
||||
|
||||
return apiLocation + "/" + endpoint;
|
||||
};
|
||||
|
||||
/**
|
||||
* Get the credential type for requests
|
||||
*
|
||||
* @returns {string} the credential type
|
||||
*/
|
||||
const credentialType = () => {
|
||||
// Development API requests use a different origin (pi.hole) since it is running off of the developer's machine.
|
||||
// Therefore, allow credentials to be used across origins when in development mode.
|
||||
return config.developmentMode ? "include" : "same-origin";
|
||||
};
|
||||
@@ -0,0 +1,104 @@
|
||||
/* 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
|
||||
* Various utilities
|
||||
*
|
||||
* This file is copyright under the latest version of the EUPL.
|
||||
* Please see LICENSE file for your rights under this license. */
|
||||
|
||||
/**
|
||||
* Pad a two digit number
|
||||
*
|
||||
* @param num the number
|
||||
* @returns {string} a padding number string
|
||||
*/
|
||||
export const padNumber = num => {
|
||||
return ("00" + num).substr(-2, 2);
|
||||
};
|
||||
|
||||
/**
|
||||
* Make a promise cancelable and repeatable
|
||||
*
|
||||
* @param promise the promise
|
||||
* @param repeat the function to call to repeat the promise
|
||||
* @param interval the amount of time to wait until repeating
|
||||
* @returns {{promise: Promise<any>, cancel(): void}} a handle on the cancelable
|
||||
* promise
|
||||
*/
|
||||
export const makeCancelable = (
|
||||
promise,
|
||||
{ repeat = null, interval = 0 } = {}
|
||||
) => {
|
||||
let hasCanceled = false;
|
||||
let repeatId = null;
|
||||
|
||||
const handle = (resolve, reject, val, isError) => {
|
||||
if (hasCanceled) reject({ isCanceled: true });
|
||||
else {
|
||||
if (isError) reject(val);
|
||||
else resolve(val);
|
||||
|
||||
if (repeat) repeatId = setTimeout(repeat, interval);
|
||||
}
|
||||
};
|
||||
|
||||
const wrappedPromise = new Promise((resolve, reject) => {
|
||||
promise.then(
|
||||
val => handle(resolve, reject, val, false),
|
||||
error => handle(resolve, reject, error, true)
|
||||
);
|
||||
});
|
||||
|
||||
return {
|
||||
promise: wrappedPromise,
|
||||
cancel() {
|
||||
clearTimeout(repeatId);
|
||||
hasCanceled = true;
|
||||
}
|
||||
};
|
||||
};
|
||||
|
||||
/**
|
||||
* Ignore canceled promises (pass into a promise's catch function)
|
||||
*
|
||||
* @param err the error from catching the promise
|
||||
*/
|
||||
export const ignoreCancel = err => {
|
||||
if (!err.isCanceled) throw err;
|
||||
};
|
||||
|
||||
/**
|
||||
* A result type, similar to Rust's Result.
|
||||
* A result can either be Ok or Err
|
||||
*/
|
||||
export const Result = {
|
||||
/**
|
||||
* Create a new Ok Result
|
||||
*
|
||||
* @param value the value of the result
|
||||
*/
|
||||
Ok: value => ({ type: "Ok", value }),
|
||||
|
||||
/**
|
||||
* Create a new Err Result
|
||||
*
|
||||
* @param value the value of the result (error)
|
||||
*/
|
||||
Err: value => ({ type: "Err", value }),
|
||||
|
||||
/**
|
||||
* Check if the result is Ok
|
||||
* @param result the result
|
||||
* @returns {boolean} if it is an Ok result
|
||||
*/
|
||||
isOk: result => result.type === "Ok",
|
||||
|
||||
/**
|
||||
* Check if the result is Err
|
||||
* @param result the result
|
||||
* @returns {boolean} if it is an Err result
|
||||
*/
|
||||
isErr: result => result.type === "Err"
|
||||
};
|
||||
-205
@@ -1,205 +0,0 @@
|
||||
/* 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
|
||||
* Various utilities including the API
|
||||
*
|
||||
* This file is copyright under the latest version of the EUPL.
|
||||
* Please see LICENSE file for your rights under this license. */
|
||||
|
||||
import config from "./config";
|
||||
|
||||
export const padNumber = num => {
|
||||
return ("00" + num).substr(-2, 2);
|
||||
};
|
||||
|
||||
export const makeCancelable = (
|
||||
promise,
|
||||
{ repeat = null, interval = 0 } = {}
|
||||
) => {
|
||||
let hasCanceled = false;
|
||||
let repeatId = null;
|
||||
|
||||
const handle = (resolve, reject, val, isError) => {
|
||||
if (hasCanceled) reject({ isCanceled: true });
|
||||
else {
|
||||
if (isError) reject(val);
|
||||
else resolve(val);
|
||||
|
||||
if (repeat) repeatId = setTimeout(repeat, interval);
|
||||
}
|
||||
};
|
||||
|
||||
const wrappedPromise = new Promise((resolve, reject) => {
|
||||
promise.then(
|
||||
val => handle(resolve, reject, val, false),
|
||||
error => handle(resolve, reject, error, true)
|
||||
);
|
||||
});
|
||||
|
||||
return {
|
||||
promise: wrappedPromise,
|
||||
cancel() {
|
||||
clearTimeout(repeatId);
|
||||
hasCanceled = true;
|
||||
}
|
||||
};
|
||||
};
|
||||
|
||||
export const ignoreCancel = err => {
|
||||
if (!err.isCanceled) throw err;
|
||||
};
|
||||
|
||||
export const api = {
|
||||
loggedIn: false,
|
||||
authenticate(key) {
|
||||
return fetch(api.urlFor("auth"), {
|
||||
headers: new Headers({ "X-Pi-hole-Authenticate": key }),
|
||||
credentials: this.credentialType()
|
||||
})
|
||||
.then(api.convertJSON)
|
||||
.then(api.checkForErrors);
|
||||
},
|
||||
logout() {
|
||||
return api.delete("auth");
|
||||
},
|
||||
getSummary() {
|
||||
return api.get("stats/summary");
|
||||
},
|
||||
getHistoryGraph() {
|
||||
return api.get("stats/overTime/history");
|
||||
},
|
||||
getClientsGraph() {
|
||||
return api.get("stats/overTime/clients");
|
||||
},
|
||||
getQueryTypes() {
|
||||
return api.get("stats/query_types");
|
||||
},
|
||||
getForwardDestinations() {
|
||||
return api.get("stats/forward_destinations");
|
||||
},
|
||||
getTopDomains() {
|
||||
return api.get("stats/top_domains");
|
||||
},
|
||||
getTopBlocked() {
|
||||
return api.get("stats/top_blocked");
|
||||
},
|
||||
getTopClients() {
|
||||
return api.get("stats/top_clients");
|
||||
},
|
||||
getHistory() {
|
||||
return api.get("stats/history");
|
||||
},
|
||||
getWhitelist() {
|
||||
return api.get("dns/whitelist");
|
||||
},
|
||||
getBlacklist() {
|
||||
return api.get("dns/blacklist");
|
||||
},
|
||||
getRegexlist() {
|
||||
return api.get("dns/regexlist");
|
||||
},
|
||||
addWhitelist(domain) {
|
||||
return api.post("dns/whitelist", { domain: domain });
|
||||
},
|
||||
addBlacklist(domain) {
|
||||
return api.post("dns/blacklist", { domain: domain });
|
||||
},
|
||||
addRegexlist(domain) {
|
||||
return api.post("dns/regexlist", { domain: domain });
|
||||
},
|
||||
removeWhitelist(domain) {
|
||||
return api.delete("dns/whitelist/" + domain);
|
||||
},
|
||||
removeBlacklist(domain) {
|
||||
return api.delete("dns/blacklist/" + domain);
|
||||
},
|
||||
removeRegexlist(domain) {
|
||||
return api.delete("dns/regexlist/" + encodeURIComponent(domain));
|
||||
},
|
||||
getStatus() {
|
||||
return api.get("dns/status");
|
||||
},
|
||||
get(url) {
|
||||
return fetch(api.urlFor(url), {
|
||||
credentials: this.credentialType()
|
||||
})
|
||||
.then(api.checkIfLoggedOut)
|
||||
.then(api.convertJSON)
|
||||
.then(api.checkForErrors);
|
||||
},
|
||||
post(url, data) {
|
||||
return fetch(api.urlFor(url), {
|
||||
method: "POST",
|
||||
body: JSON.stringify(data),
|
||||
headers: new Headers({ "Content-Type": "application/json" }),
|
||||
credentials: this.credentialType()
|
||||
})
|
||||
.then(api.checkIfLoggedOut)
|
||||
.then(api.convertJSON)
|
||||
.then(api.checkForErrors);
|
||||
},
|
||||
delete(url) {
|
||||
return fetch(api.urlFor(url), {
|
||||
method: "DELETE",
|
||||
credentials: this.credentialType()
|
||||
})
|
||||
.then(api.checkIfLoggedOut)
|
||||
.then(api.convertJSON)
|
||||
.then(api.checkForErrors);
|
||||
},
|
||||
getNetworkInfo() {
|
||||
return api.get("settings/network");
|
||||
},
|
||||
getVersion() {
|
||||
return api.get("version");
|
||||
},
|
||||
getFTLdb() {
|
||||
return api.get("settings/ftldb");
|
||||
},
|
||||
getDNSInfo() {
|
||||
return api.get("settings/dns");
|
||||
},
|
||||
getDHCPInfo() {
|
||||
return api.get("settings/dhcp");
|
||||
},
|
||||
/**
|
||||
* If the user is logged in, check if the user's session has lapsed.
|
||||
* If so, log them out and refresh the page.
|
||||
*/
|
||||
checkIfLoggedOut(response) {
|
||||
if (api.loggedIn && response.status === 401) {
|
||||
// Clear the user's old session and refresh the page
|
||||
document.cookie =
|
||||
"user_id=; Path=/; Expires=Thu, 01 Jan 1970 00:00:01 GMT;";
|
||||
window.location.reload();
|
||||
return Promise.reject({ isCanceled: true });
|
||||
}
|
||||
|
||||
return Promise.resolve(response);
|
||||
},
|
||||
async convertJSON(data) {
|
||||
if (!data.ok) return Promise.reject({ data, json: await data.json() });
|
||||
else return data.json();
|
||||
},
|
||||
checkForErrors(data) {
|
||||
if (data.error) {
|
||||
return Promise.reject(data.error);
|
||||
}
|
||||
return Promise.resolve(data);
|
||||
},
|
||||
urlFor(endpoint) {
|
||||
let apiLocation;
|
||||
|
||||
if (config.fakeAPI) apiLocation = process.env.PUBLIC_URL + "/fakeAPI";
|
||||
else apiLocation = "/admin/api";
|
||||
|
||||
return apiLocation + "/" + endpoint;
|
||||
},
|
||||
credentialType() {
|
||||
// Development API requests use a different origin (pi.hole) since it is running off of the developer's machine.
|
||||
// Therefore, allow credentials to be used across origins when in development mode.
|
||||
return config.developmentMode ? "include" : "same-origin";
|
||||
}
|
||||
};
|
||||
+1
-1
@@ -8,7 +8,7 @@
|
||||
* This file is copyright under the latest version of the EUPL.
|
||||
* Please see LICENSE file for your rights under this license. */
|
||||
|
||||
import { api } from "./utils";
|
||||
import api from "./util/api";
|
||||
|
||||
// This is a dumb test used to set up the next test,
|
||||
// which checks that the logged in state is reset before each test
|
||||
|
||||
@@ -10,7 +10,7 @@
|
||||
|
||||
import React from "react";
|
||||
import { translate } from "react-i18next";
|
||||
import { api } from "../utils";
|
||||
import api from "../util/api";
|
||||
import ListPage from "../components/list/ListPage";
|
||||
import { isValidDomain } from "../validate";
|
||||
|
||||
|
||||
@@ -17,7 +17,7 @@ import ForwardDestinationsChart from "../components/dashboard/ForwardDestination
|
||||
import TopDomains from "../components/dashboard/TopDomains";
|
||||
import TopBlocked from "../components/dashboard/TopBlocked";
|
||||
import TopClients from "../components/dashboard/TopClients";
|
||||
import { api } from "../utils";
|
||||
import api from "../util/api";
|
||||
|
||||
export default () => (
|
||||
<div className="animated fadeIn">
|
||||
|
||||
+1
-1
@@ -11,7 +11,7 @@
|
||||
import React, { Component, Fragment } from "react";
|
||||
import { Redirect } from "react-router-dom";
|
||||
import sha from "sha.js";
|
||||
import { api } from "../utils";
|
||||
import api from "../util/api";
|
||||
import logo from "../img/logo.svg";
|
||||
import { routes } from "../routes";
|
||||
import ForgotPassword from "../components/login/ForgotPassword";
|
||||
|
||||
+1
-1
@@ -10,7 +10,7 @@
|
||||
|
||||
import React, { Component } from "react";
|
||||
import { Redirect } from "react-router-dom";
|
||||
import { api } from "../utils";
|
||||
import api from "../util/api";
|
||||
import config from "../config";
|
||||
|
||||
export default class Logout extends Component {
|
||||
|
||||
@@ -11,7 +11,7 @@
|
||||
import React from "react";
|
||||
import { translate } from "react-i18next";
|
||||
import ListPage from "../components/list/ListPage";
|
||||
import { api } from "../utils";
|
||||
import api from "../util/api";
|
||||
import { isValidRegex } from "../validate";
|
||||
|
||||
const Regexlist = props => {
|
||||
|
||||
@@ -11,7 +11,7 @@
|
||||
import React from "react";
|
||||
import { translate } from "react-i18next";
|
||||
import ListPage from "../components/list/ListPage";
|
||||
import { api } from "../utils";
|
||||
import api from "../util/api";
|
||||
import { isValidDomain } from "../validate";
|
||||
|
||||
const Whitelist = props => {
|
||||
|
||||
Reference in New Issue
Block a user