mirror of
https://github.com/gosticks/web.git
synced 2026-08-12 12:10:20 +00:00
Merge branch 'development' into l10n_development
This commit is contained in:
Generated
+5706
-7223
File diff suppressed because it is too large
Load Diff
+16
-18
@@ -1,24 +1,23 @@
|
||||
{
|
||||
"name": "pi-hole_web",
|
||||
"version": "1.0.0",
|
||||
"version": "0.1.0",
|
||||
"private": true,
|
||||
"homepage": ".",
|
||||
"devDependencies": {
|
||||
"@types/chart.js": "^2.7.42",
|
||||
"@types/enzyme": "^3.1.15",
|
||||
"@types/enzyme-adapter-react-16": "^1.0.3",
|
||||
"@types/enzyme": "^3.9.3",
|
||||
"@types/enzyme-adapter-react-16": "^1.0.5",
|
||||
"@types/fetch-mock": "^7.2.1",
|
||||
"@types/i18next-browser-languagedetector": "^2.0.1",
|
||||
"@types/i18next-xhr-backend": "^1.4.1",
|
||||
"@types/jest": "^23.3.13",
|
||||
"@types/jest": "^24.0.13",
|
||||
"@types/lodash.debounce": "^4.0.4",
|
||||
"@types/node": "^10.12.18",
|
||||
"@types/react": "^16.8.8",
|
||||
"@types/node": "^12.0.2",
|
||||
"@types/react": "^16.8.18",
|
||||
"@types/react-bootstrap-daterangepicker": "0.0.26",
|
||||
"@types/react-bootstrap-typeahead": "^3.4.0",
|
||||
"@types/react-dom": "^16.8.2",
|
||||
"@types/react-router-dom": "^4.3.1",
|
||||
"@types/react-table": "^6.7.21",
|
||||
"@types/react-router-dom": "^4.3.3",
|
||||
"@types/react-table": "^6.8.1",
|
||||
"@types/reactstrap": "^6.4.4",
|
||||
"@types/sha.js": "^2.4.0",
|
||||
"enzyme": "^3.8.0",
|
||||
@@ -33,32 +32,31 @@
|
||||
"node-sass-chokidar": "^1.3.4",
|
||||
"npm-run-all": "^4.1.5",
|
||||
"prettier": "^1.15.3",
|
||||
"react-scripts": "^2.1.8",
|
||||
"react-test-renderer": "^16.7.0",
|
||||
"react-scripts": "^3.0.1",
|
||||
"react-test-renderer": "^16.8.6",
|
||||
"typescript": "^3.2.4"
|
||||
},
|
||||
"dependencies": {
|
||||
"@coreui/coreui": "^2.1.6",
|
||||
"@coreui/coreui-plugin-chartjs-custom-tooltips": "^1.2.0",
|
||||
"@coreui/coreui": "^2.1.9",
|
||||
"@coreui/icons": "0.3.0",
|
||||
"@coreui/react": "^2.1.3",
|
||||
"@fortawesome/fontawesome-free": "^5.6.3",
|
||||
"bootstrap": "^4.2.1",
|
||||
"bootstrap": "^4.3.1",
|
||||
"bootstrap-daterangepicker": "^3.0.3",
|
||||
"chart.js": "^2.7.3",
|
||||
"i18next": "^13.1.4",
|
||||
"i18next-browser-languagedetector": "^2.2.4",
|
||||
"i18next-xhr-backend": "^1.5.1",
|
||||
"i18next-xhr-backend": "^2.0.1",
|
||||
"ionicons": "^4.5.1",
|
||||
"lodash.debounce": "^4.0.8",
|
||||
"moment": "^2.23.0",
|
||||
"prop-types": "^15.6.2",
|
||||
"react": "^16.8.4",
|
||||
"react": "^16.8.6",
|
||||
"react-bootstrap-daterangepicker": "^4.1.0",
|
||||
"react-bootstrap-typeahead": "^3.2.4",
|
||||
"react-chartjs-2": "^2.7.4",
|
||||
"react-dom": "^16.8.4",
|
||||
"react-i18next": "^9.0.3",
|
||||
"react-dom": "^16.8.6",
|
||||
"react-i18next": "^9.0.7",
|
||||
"react-router-dom": "^4.3.1",
|
||||
"react-table": "^6.8.6",
|
||||
"react-transition-group": "^2.5.3",
|
||||
|
||||
@@ -26,5 +26,6 @@
|
||||
"Processing...": "Processing...",
|
||||
"Successfully saved settings": "Successfully saved settings",
|
||||
"Detected custom upstream server": "Detected custom upstream server",
|
||||
"DNS Options": "DNS Options"
|
||||
"DNS Options": "DNS Options",
|
||||
"Rapid Commit": "Rapid Commit"
|
||||
}
|
||||
@@ -343,7 +343,8 @@ function getDHCPInfo() {
|
||||
.word()
|
||||
.toLowerCase()
|
||||
.split(" ", 2)[0],
|
||||
ipv6_support: faker.random.boolean()
|
||||
ipv6_support: faker.random.boolean(),
|
||||
rapid_commit: faker.random.boolean()
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -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 React, { FunctionComponent } from "react";
|
||||
import React from "react";
|
||||
|
||||
export type AlertType = "info" | "success" | "danger";
|
||||
|
||||
@@ -16,23 +16,27 @@ export interface AlertProps {
|
||||
type: AlertType;
|
||||
onClick: () => void;
|
||||
message: string;
|
||||
dismissible: boolean;
|
||||
}
|
||||
|
||||
const Alert: FunctionComponent<AlertProps> = (props: AlertProps) => {
|
||||
const Alert = (props: AlertProps) => {
|
||||
const dismissClass = props.dismissible ? "alert-dismissible" : "";
|
||||
|
||||
return (
|
||||
<div
|
||||
className={"alert alert-" + props.type + " alert-dismissible fade show"}
|
||||
>
|
||||
<button type="button" className="close" onClick={props.onClick}>
|
||||
×
|
||||
</button>
|
||||
<div className={`alert alert-${props.type} ${dismissClass} fade show`}>
|
||||
{props.dismissible ? (
|
||||
<button type="button" className="close" onClick={props.onClick}>
|
||||
×
|
||||
</button>
|
||||
) : null}
|
||||
{props.message}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
Alert.defaultProps = {
|
||||
onClick: () => {}
|
||||
onClick: () => {},
|
||||
dismissible: true
|
||||
};
|
||||
|
||||
export default Alert;
|
||||
|
||||
@@ -15,7 +15,10 @@ import { WithNamespaces, withNamespaces } from "react-i18next";
|
||||
import NavButton from "./NavButton";
|
||||
import NavDropdown from "./NavDropdown";
|
||||
import { StatusContext } from "./context/StatusContext";
|
||||
import { CancelablePromise, makeCancelable } from "../../util";
|
||||
import {
|
||||
CancelablePromise,
|
||||
makeCancelable
|
||||
} from "../../util/CancelablePromise";
|
||||
import api from "../../util/api";
|
||||
import {
|
||||
Button,
|
||||
@@ -49,7 +52,7 @@ class EnableDisable extends Component<EnableDisableProps, EnableDisableState> {
|
||||
customMultiplier: 60
|
||||
};
|
||||
|
||||
private updateHandler: CancelablePromise<ApiStatus> | undefined;
|
||||
private updateHandler: CancelablePromise<ApiSuccessResponse> | undefined;
|
||||
|
||||
/**
|
||||
* Convert a status action into a status. ex. "enable" -> "enabled"
|
||||
|
||||
@@ -9,7 +9,11 @@
|
||||
* Please see LICENSE file for your rights under this license. */
|
||||
|
||||
import { Component, ReactNode } from "react";
|
||||
import { CancelablePromise, ignoreCancel, makeCancelable } from "../../util";
|
||||
import {
|
||||
CancelablePromise,
|
||||
ignoreCancel,
|
||||
makeCancelable
|
||||
} from "../../util/CancelablePromise";
|
||||
import { Err, Ok, Result } from "../../util/result";
|
||||
|
||||
export interface WithAPIDataProps<T> {
|
||||
|
||||
@@ -12,7 +12,7 @@ import React, { Component, RefObject } from "react";
|
||||
import ReactDOM from "react-dom";
|
||||
import { Line } from "react-chartjs-2";
|
||||
import { WithNamespaces, withNamespaces } from "react-i18next";
|
||||
import { getIntervalForRange, padNumber } from "../../util";
|
||||
import { getIntervalForRange, padNumber } from "../../util/graphUtils";
|
||||
import api from "../../util/api";
|
||||
import ChartTooltip from "./ChartTooltip";
|
||||
import { WithAPIData } from "../common/WithAPIData";
|
||||
|
||||
@@ -10,7 +10,7 @@
|
||||
|
||||
import React, { Component } from "react";
|
||||
import { WithNamespaces, withNamespaces } from "react-i18next";
|
||||
import { getIntervalForRange, padNumber } from "../../util";
|
||||
import { getIntervalForRange, padNumber } from "../../util/graphUtils";
|
||||
import api from "../../util/api";
|
||||
import { WithAPIData } from "../common/WithAPIData";
|
||||
import { ChartData, ChartOptions, TimeUnit } from "chart.js";
|
||||
|
||||
@@ -12,6 +12,7 @@ import React from "react";
|
||||
import { WithNamespaces, withNamespaces } from "react-i18next";
|
||||
import api from "../../util/api";
|
||||
import { Button } from "reactstrap";
|
||||
import Alert from "../common/Alert";
|
||||
|
||||
export interface DomainListProps extends WithNamespaces {
|
||||
domains: string[];
|
||||
@@ -54,9 +55,11 @@ const DomainList = ({ domains, onRemove, t }: DomainListProps) => {
|
||||
body = domains.map(mapDomainsToListItems);
|
||||
} else {
|
||||
body = (
|
||||
<div className="alert alert-info" role="alert">
|
||||
{t("There are no domains in this list")}
|
||||
</div>
|
||||
<Alert
|
||||
type="info"
|
||||
message={t("There are no domains in this list")}
|
||||
dismissible={false}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -13,15 +13,19 @@ import { WithNamespaces, withNamespaces } from "react-i18next";
|
||||
import DomainInput from "./DomainInput";
|
||||
import Alert, { AlertType } from "../common/Alert";
|
||||
import DomainList from "./DomainList";
|
||||
import { CancelablePromise, ignoreCancel, makeCancelable } from "../../util";
|
||||
import {
|
||||
CancelablePromise,
|
||||
ignoreCancel,
|
||||
makeCancelable
|
||||
} from "../../util/CancelablePromise";
|
||||
|
||||
export interface ListPageProps extends WithNamespaces {
|
||||
title: string;
|
||||
note?: {} | string;
|
||||
placeholder: string;
|
||||
add: (domain: string) => Promise<any | never>;
|
||||
refresh: () => Promise<any | never>;
|
||||
remove: (domain: string) => Promise<any | never>;
|
||||
onAdd: (domain: string) => Promise<any | never>;
|
||||
onRefresh: () => Promise<any | never>;
|
||||
onRemove: (domain: string) => Promise<any | never>;
|
||||
isValid: (domain: string) => boolean;
|
||||
validationErrorMsg: string;
|
||||
}
|
||||
@@ -56,7 +60,7 @@ export class ListPage extends Component<ListPageProps, ListPageState> {
|
||||
const prevDomains = this.state.domains.slice();
|
||||
|
||||
// Try to add the domain
|
||||
this.addHandler = makeCancelable(this.props.add(domain));
|
||||
this.addHandler = makeCancelable(this.props.onAdd(domain));
|
||||
this.addHandler.promise
|
||||
.then(() => {
|
||||
this.onAdded(domain);
|
||||
@@ -113,7 +117,7 @@ export class ListPage extends Component<ListPageProps, ListPageState> {
|
||||
if (this.state.domains.includes(domain)) {
|
||||
const prevDomains = this.state.domains.slice();
|
||||
|
||||
this.removeHandler = makeCancelable(this.props.remove(domain));
|
||||
this.removeHandler = makeCancelable(this.props.onRemove(domain));
|
||||
this.removeHandler.promise.catch(ignoreCancel).catch(() => {
|
||||
this.onRemoveFailed(domain, prevDomains);
|
||||
});
|
||||
@@ -123,7 +127,7 @@ export class ListPage extends Component<ListPageProps, ListPageState> {
|
||||
};
|
||||
|
||||
onRefresh = () => {
|
||||
this.refreshHandler = makeCancelable(this.props.refresh());
|
||||
this.refreshHandler = makeCancelable(this.props.onRefresh());
|
||||
this.refreshHandler.promise
|
||||
.then(data => {
|
||||
this.setState({ domains: data });
|
||||
|
||||
@@ -12,6 +12,7 @@ import React from "react";
|
||||
import { shallow } from "enzyme";
|
||||
import DomainList from "../DomainList";
|
||||
import api from "../../../util/api";
|
||||
import Alert from "../../common/Alert";
|
||||
|
||||
const domains = ["domain1.com", "domain2.com", "domain3.com"];
|
||||
|
||||
@@ -26,9 +27,11 @@ it("shows a list of domains", () => {
|
||||
it("shows an alert if there are no domains", () => {
|
||||
const wrapper = shallow(<DomainList domains={[]} onRemove={jest.fn()} />);
|
||||
|
||||
expect(wrapper.find("li")).toHaveLength(0);
|
||||
expect(wrapper.find("ul").childAt(0)).toHaveClassName("alert-info");
|
||||
expect(wrapper).toIncludeText("There are no domains in this list");
|
||||
expect(wrapper.find("li")).not.toExist();
|
||||
expect(wrapper.find(Alert)).toExist();
|
||||
expect(wrapper.find(Alert).props().message).toEqual(
|
||||
"There are no domains in this list"
|
||||
);
|
||||
});
|
||||
|
||||
it("does not have a delete button when not logged in", () => {
|
||||
@@ -36,12 +39,7 @@ it("does not have a delete button when not logged in", () => {
|
||||
<DomainList domains={domains} onRemove={jest.fn()} />
|
||||
);
|
||||
|
||||
expect(
|
||||
wrapper
|
||||
.find("ul")
|
||||
.childAt(0)
|
||||
.find("button")
|
||||
).not.toExist();
|
||||
expect(wrapper.find("Button")).not.toExist();
|
||||
});
|
||||
|
||||
it("has a delete button when logged in", () => {
|
||||
|
||||
@@ -15,6 +15,9 @@ import ListPage, {
|
||||
ListPageProps,
|
||||
ListPageState
|
||||
} from "../ListPage";
|
||||
import Alert from "../../common/Alert";
|
||||
import DomainInput from "../DomainInput";
|
||||
import DomainList from "../DomainList";
|
||||
|
||||
const ignoreAPI = global.ignoreAPI;
|
||||
const tick = global.tick;
|
||||
@@ -32,9 +35,9 @@ it("shows the title", () => {
|
||||
title={title}
|
||||
placeholder=""
|
||||
note=""
|
||||
add={ignoreAPI}
|
||||
refresh={ignoreAPI}
|
||||
remove={ignoreAPI}
|
||||
onAdd={ignoreAPI}
|
||||
onRefresh={ignoreAPI}
|
||||
onRemove={ignoreAPI}
|
||||
isValid={jest.fn()}
|
||||
validationErrorMsg=""
|
||||
/>
|
||||
@@ -50,15 +53,15 @@ it("shows the placeholder", () => {
|
||||
title=""
|
||||
placeholder={placeholder}
|
||||
note=""
|
||||
add={ignoreAPI}
|
||||
refresh={ignoreAPI}
|
||||
remove={ignoreAPI}
|
||||
onAdd={ignoreAPI}
|
||||
onRefresh={ignoreAPI}
|
||||
onRemove={ignoreAPI}
|
||||
isValid={jest.fn()}
|
||||
validationErrorMsg=""
|
||||
/>
|
||||
);
|
||||
|
||||
expect(wrapper.find("DomainInput")).toHaveProp("placeholder", placeholder);
|
||||
expect(wrapper.find(DomainInput)).toHaveProp("placeholder", placeholder);
|
||||
});
|
||||
|
||||
it("shows the note", () => {
|
||||
@@ -68,9 +71,9 @@ it("shows the note", () => {
|
||||
title=""
|
||||
placeholder=""
|
||||
note={note}
|
||||
add={ignoreAPI}
|
||||
refresh={ignoreAPI}
|
||||
remove={ignoreAPI}
|
||||
onAdd={ignoreAPI}
|
||||
onRefresh={ignoreAPI}
|
||||
onRemove={ignoreAPI}
|
||||
isValid={jest.fn()}
|
||||
validationErrorMsg=""
|
||||
/>
|
||||
@@ -85,15 +88,105 @@ it("starts with no alerts shown", () => {
|
||||
title=""
|
||||
placeholder=""
|
||||
note=""
|
||||
add={ignoreAPI}
|
||||
refresh={ignoreAPI}
|
||||
remove={ignoreAPI}
|
||||
onAdd={ignoreAPI}
|
||||
onRefresh={ignoreAPI}
|
||||
onRemove={ignoreAPI}
|
||||
isValid={jest.fn()}
|
||||
validationErrorMsg=""
|
||||
/>
|
||||
);
|
||||
|
||||
expect(wrapper.find("Alert")).toHaveLength(0);
|
||||
expect(wrapper.find(Alert)).not.toExist();
|
||||
});
|
||||
|
||||
it("hides the alert if closed", () => {
|
||||
const wrapper: ListPageWrapper = shallow(
|
||||
<ListPage
|
||||
title=""
|
||||
placeholder=""
|
||||
note=""
|
||||
onAdd={ignoreAPI}
|
||||
onRefresh={ignoreAPI}
|
||||
onRemove={ignoreAPI}
|
||||
isValid={jest.fn()}
|
||||
validationErrorMsg=""
|
||||
/>
|
||||
);
|
||||
|
||||
// Show an error message
|
||||
wrapper.instance().onAlreadyAdded("domain");
|
||||
|
||||
// Now the alert is shown
|
||||
const alert = wrapper.find(Alert);
|
||||
expect(alert).toExist();
|
||||
|
||||
// Hide the alert
|
||||
alert.props().onClick();
|
||||
|
||||
expect(wrapper.find(Alert)).not.toExist();
|
||||
});
|
||||
|
||||
it("cancels requests when un-mounting", async () => {
|
||||
const wrapper: ListPageWrapper = shallow(
|
||||
<ListPage
|
||||
title=""
|
||||
placeholder=""
|
||||
note=""
|
||||
onAdd={ignoreAPI}
|
||||
onRefresh={() => Promise.resolve(["domain"])}
|
||||
onRemove={ignoreAPI}
|
||||
isValid={jest.fn()}
|
||||
validationErrorMsg=""
|
||||
/>
|
||||
);
|
||||
|
||||
// Load the domains into state
|
||||
await tick();
|
||||
|
||||
// Initiate requests to refresh, add, and remove domains
|
||||
wrapper.instance().onRefresh();
|
||||
wrapper.instance().onEnter("domain2");
|
||||
wrapper.instance().onRemove("domain");
|
||||
|
||||
// Spy on the handlers
|
||||
// Casting instance to any to access private fields
|
||||
const instance = wrapper.instance() as any;
|
||||
const cancelRefreshSpy = jest.spyOn(instance.refreshHandler, "cancel");
|
||||
const cancelAddSpy = jest.spyOn(instance.addHandler, "cancel");
|
||||
const cancelRemoveSpy = jest.spyOn(instance.removeHandler, "cancel");
|
||||
|
||||
// Unmount, which should cancel the requests
|
||||
wrapper.unmount();
|
||||
|
||||
expect(cancelRefreshSpy).toHaveBeenCalled();
|
||||
expect(cancelAddSpy).toHaveBeenCalled();
|
||||
expect(cancelRemoveSpy).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("shows a validation message as an error", () => {
|
||||
const validationError = "test message";
|
||||
const wrapper = shallow(
|
||||
<ListPage
|
||||
title=""
|
||||
placeholder=""
|
||||
note=""
|
||||
onAdd={ignoreAPI}
|
||||
onRefresh={ignoreAPI}
|
||||
onRemove={ignoreAPI}
|
||||
isValid={jest.fn()}
|
||||
validationErrorMsg={validationError}
|
||||
/>
|
||||
);
|
||||
|
||||
wrapper
|
||||
.find(DomainInput)
|
||||
.props()
|
||||
.onValidationError();
|
||||
|
||||
const alert = wrapper.find(Alert);
|
||||
expect(alert).toExist();
|
||||
expect(alert.props().message).toEqual(validationError);
|
||||
expect(alert.props().type).toEqual("danger");
|
||||
});
|
||||
|
||||
it("loads domains after mounting", async () => {
|
||||
@@ -103,42 +196,38 @@ it("loads domains after mounting", async () => {
|
||||
title=""
|
||||
placeholder=""
|
||||
note=""
|
||||
add={ignoreAPI}
|
||||
refresh={() => Promise.resolve(domains)}
|
||||
remove={ignoreAPI}
|
||||
onAdd={ignoreAPI}
|
||||
onRefresh={() => Promise.resolve(domains)}
|
||||
onRemove={ignoreAPI}
|
||||
isValid={jest.fn()}
|
||||
validationErrorMsg=""
|
||||
/>
|
||||
);
|
||||
|
||||
await tick();
|
||||
wrapper.update();
|
||||
|
||||
expect(wrapper.find("DomainList")).toHaveProp("domains", domains);
|
||||
expect(wrapper.find(DomainList)).toHaveProp("domains", domains);
|
||||
});
|
||||
|
||||
it("checks if the domain was already added", async () => {
|
||||
const domains = ["domain1", "domain2.com", "domain3.net"];
|
||||
const onAlreadyAdded = jest.fn();
|
||||
const wrapper: ListPageWrapper = shallow(
|
||||
<ListPage
|
||||
title=""
|
||||
placeholder=""
|
||||
note=""
|
||||
add={ignoreAPI}
|
||||
refresh={() => Promise.resolve(domains)}
|
||||
remove={ignoreAPI}
|
||||
onAdd={ignoreAPI}
|
||||
onRefresh={() => Promise.resolve(domains)}
|
||||
onRemove={ignoreAPI}
|
||||
isValid={jest.fn()}
|
||||
validationErrorMsg=""
|
||||
/>
|
||||
);
|
||||
const onAlreadyAdded = jest.spyOn(wrapper.instance(), "onAlreadyAdded");
|
||||
|
||||
// Setup with domains (wait for promise to resolve) and mock function
|
||||
// Setup with domains (wait for promise to resolve)
|
||||
await tick();
|
||||
wrapper.instance().onAlreadyAdded = onAlreadyAdded;
|
||||
wrapper.update();
|
||||
|
||||
// Test onEnter
|
||||
wrapper.instance().onEnter(domains[0]);
|
||||
|
||||
expect(onAlreadyAdded).toHaveBeenCalledWith(domains[0]);
|
||||
@@ -146,15 +235,15 @@ it("checks if the domain was already added", async () => {
|
||||
|
||||
it("calls the add prop when adding a domain", () => {
|
||||
const domain = "domain";
|
||||
const add = jest.fn(ignoreAPI);
|
||||
const onAdd = jest.fn(ignoreAPI);
|
||||
const wrapper: ListPageWrapper = shallow(
|
||||
<ListPage
|
||||
title=""
|
||||
placeholder=""
|
||||
note=""
|
||||
add={add}
|
||||
refresh={ignoreAPI}
|
||||
remove={ignoreAPI}
|
||||
onAdd={onAdd}
|
||||
onRefresh={ignoreAPI}
|
||||
onRemove={ignoreAPI}
|
||||
isValid={jest.fn()}
|
||||
validationErrorMsg=""
|
||||
/>
|
||||
@@ -162,27 +251,25 @@ it("calls the add prop when adding a domain", () => {
|
||||
|
||||
wrapper.instance().onEnter(domain);
|
||||
|
||||
expect(add).toHaveBeenCalledWith(domain);
|
||||
expect(onAdd).toHaveBeenCalledWith(domain);
|
||||
});
|
||||
|
||||
it("calls onAdding when adding a domain", () => {
|
||||
const domain = "domain";
|
||||
const onAdding = jest.fn();
|
||||
const wrapper: ListPageWrapper = shallow(
|
||||
<ListPage
|
||||
title=""
|
||||
placeholder=""
|
||||
note=""
|
||||
add={ignoreAPI}
|
||||
refresh={ignoreAPI}
|
||||
remove={ignoreAPI}
|
||||
onAdd={ignoreAPI}
|
||||
onRefresh={ignoreAPI}
|
||||
onRemove={ignoreAPI}
|
||||
isValid={jest.fn()}
|
||||
validationErrorMsg=""
|
||||
/>
|
||||
);
|
||||
const onAdding = jest.spyOn(wrapper.instance(), "onAdding");
|
||||
|
||||
wrapper.instance().onAdding = onAdding;
|
||||
wrapper.update();
|
||||
wrapper.instance().onEnter(domain);
|
||||
|
||||
expect(onAdding).toHaveBeenCalledWith(domain);
|
||||
@@ -190,22 +277,20 @@ it("calls onAdding when adding a domain", () => {
|
||||
|
||||
it("calls onAdded after API request succeeds", async () => {
|
||||
const domain = "domain";
|
||||
const onAdded = jest.fn();
|
||||
const wrapper: ListPageWrapper = shallow(
|
||||
<ListPage
|
||||
title=""
|
||||
placeholder=""
|
||||
note=""
|
||||
add={() => Promise.resolve()}
|
||||
refresh={ignoreAPI}
|
||||
remove={ignoreAPI}
|
||||
onAdd={() => Promise.resolve()}
|
||||
onRefresh={ignoreAPI}
|
||||
onRemove={ignoreAPI}
|
||||
isValid={jest.fn()}
|
||||
validationErrorMsg=""
|
||||
/>
|
||||
);
|
||||
const onAdded = jest.spyOn(wrapper.instance(), "onAdded");
|
||||
|
||||
wrapper.instance().onAdded = onAdded;
|
||||
wrapper.update();
|
||||
wrapper.instance().onEnter(domain);
|
||||
await tick();
|
||||
|
||||
@@ -214,22 +299,20 @@ it("calls onAdded after API request succeeds", async () => {
|
||||
|
||||
it("calls onAddFailed after API request fails", async () => {
|
||||
const domain = "domain";
|
||||
const onAddFailed = jest.fn();
|
||||
const wrapper: ListPageWrapper = shallow(
|
||||
<ListPage
|
||||
title=""
|
||||
placeholder=""
|
||||
note=""
|
||||
add={() => Promise.reject({})}
|
||||
refresh={ignoreAPI}
|
||||
remove={ignoreAPI}
|
||||
onAdd={() => Promise.reject({})}
|
||||
onRefresh={ignoreAPI}
|
||||
onRemove={ignoreAPI}
|
||||
isValid={jest.fn()}
|
||||
validationErrorMsg=""
|
||||
/>
|
||||
);
|
||||
const onAddFailed = jest.spyOn(wrapper.instance(), "onAddFailed");
|
||||
|
||||
wrapper.instance().onAddFailed = onAddFailed;
|
||||
wrapper.update();
|
||||
wrapper.instance().onEnter(domain);
|
||||
await tick();
|
||||
|
||||
@@ -243,16 +326,15 @@ it("adds the domain in onAdded", async () => {
|
||||
title=""
|
||||
placeholder=""
|
||||
note=""
|
||||
add={() => Promise.resolve()}
|
||||
refresh={ignoreAPI}
|
||||
remove={ignoreAPI}
|
||||
onAdd={() => Promise.resolve()}
|
||||
onRefresh={ignoreAPI}
|
||||
onRemove={ignoreAPI}
|
||||
isValid={jest.fn()}
|
||||
validationErrorMsg=""
|
||||
/>
|
||||
);
|
||||
|
||||
wrapper.instance().onEnter(domain);
|
||||
wrapper.update();
|
||||
await tick();
|
||||
|
||||
expect(wrapper.state().domains).toEqual([domain]);
|
||||
@@ -265,60 +347,84 @@ it("resets the domains when adding failed", async () => {
|
||||
title=""
|
||||
placeholder=""
|
||||
note=""
|
||||
add={() => Promise.reject({})}
|
||||
refresh={ignoreAPI}
|
||||
remove={ignoreAPI}
|
||||
onAdd={() => Promise.reject({})}
|
||||
onRefresh={ignoreAPI}
|
||||
onRemove={ignoreAPI}
|
||||
isValid={jest.fn()}
|
||||
validationErrorMsg=""
|
||||
/>
|
||||
);
|
||||
|
||||
wrapper.instance().onEnter(domain);
|
||||
wrapper.update();
|
||||
await tick();
|
||||
|
||||
expect(wrapper.state().domains).toEqual([]);
|
||||
});
|
||||
|
||||
it("removes the domain when onRemoved is called", async () => {
|
||||
const domain = "domain";
|
||||
const domains = [domain];
|
||||
it("does not remove the domain if it is not present", async () => {
|
||||
const domain = "domain1";
|
||||
const domains = ["domain2"];
|
||||
const wrapper: ListPageWrapper = shallow(
|
||||
<ListPage
|
||||
title=""
|
||||
placeholder=""
|
||||
note=""
|
||||
add={ignoreAPI}
|
||||
refresh={() => Promise.resolve(domains)}
|
||||
remove={ignoreAPI}
|
||||
onAdd={ignoreAPI}
|
||||
onRefresh={() => Promise.resolve(domains)}
|
||||
onRemove={ignoreAPI}
|
||||
isValid={jest.fn()}
|
||||
validationErrorMsg=""
|
||||
/>
|
||||
);
|
||||
|
||||
wrapper.instance().onRemoved(domain);
|
||||
wrapper.update();
|
||||
await tick();
|
||||
wrapper.instance().onRemove(domain);
|
||||
|
||||
expect(wrapper.state().domains).toEqual([]);
|
||||
expect(wrapper.state().domains).toEqual(domains);
|
||||
});
|
||||
|
||||
it("resets the domains when removal failed", () => {
|
||||
it("removes the domain from state when onRemove is called", async () => {
|
||||
const domain = "domain";
|
||||
const domain2 = "domain2";
|
||||
const domains = [domain, domain2];
|
||||
const wrapper: ListPageWrapper = shallow(
|
||||
<ListPage
|
||||
title=""
|
||||
placeholder=""
|
||||
note=""
|
||||
onAdd={ignoreAPI}
|
||||
onRefresh={() => Promise.resolve(domains)}
|
||||
onRemove={() => Promise.resolve()}
|
||||
isValid={jest.fn()}
|
||||
validationErrorMsg=""
|
||||
/>
|
||||
);
|
||||
|
||||
await tick();
|
||||
wrapper.instance().onRemove(domain);
|
||||
|
||||
expect(wrapper.state().domains).toEqual([domain2]);
|
||||
});
|
||||
|
||||
it("resets the domains when removal failed", async () => {
|
||||
const domain = "domain";
|
||||
const domains = [domain];
|
||||
const wrapper: ListPageWrapper = shallow(
|
||||
<ListPage
|
||||
title=""
|
||||
placeholder=""
|
||||
note=""
|
||||
add={ignoreAPI}
|
||||
refresh={() => Promise.resolve(domains)}
|
||||
remove={ignoreAPI}
|
||||
onAdd={ignoreAPI}
|
||||
onRefresh={() => Promise.resolve(domains)}
|
||||
onRemove={() => Promise.reject()}
|
||||
isValid={jest.fn()}
|
||||
validationErrorMsg=""
|
||||
/>
|
||||
);
|
||||
wrapper.instance().onRemoveFailed(domain, domains);
|
||||
wrapper.update();
|
||||
|
||||
await tick();
|
||||
wrapper.instance().onRemove(domain);
|
||||
await tick();
|
||||
|
||||
expect(wrapper.state().domains).toEqual(domains);
|
||||
});
|
||||
|
||||
@@ -20,18 +20,18 @@ import i18next from "i18next";
|
||||
import { WithNamespaces, withNamespaces } from "react-i18next";
|
||||
import debounce from "lodash.debounce";
|
||||
import moment from "moment";
|
||||
import {
|
||||
CancelablePromise,
|
||||
ignoreCancel,
|
||||
makeCancelable,
|
||||
padNumber
|
||||
} from "../../util";
|
||||
import { padNumber } from "../../util/graphUtils";
|
||||
import api from "../../util/api";
|
||||
import { dateRanges } from "../../util/dateRanges";
|
||||
import { TranslatedTimeRangeSelector } from "../dashboard/TimeRangeSelector";
|
||||
import { TimeRange } from "../common/context/TimeRangeContext";
|
||||
import "react-table/react-table.css";
|
||||
import "bootstrap-daterangepicker/daterangepicker.css";
|
||||
import {
|
||||
CancelablePromise,
|
||||
ignoreCancel,
|
||||
makeCancelable
|
||||
} from "../../util/CancelablePromise";
|
||||
|
||||
export interface QueryLogState {
|
||||
history: Array<ApiQuery>;
|
||||
|
||||
@@ -10,7 +10,11 @@
|
||||
|
||||
import React, { ChangeEvent, Component, FormEvent } from "react";
|
||||
import { WithNamespaces, withNamespaces } from "react-i18next";
|
||||
import { CancelablePromise, ignoreCancel, makeCancelable } from "../../util";
|
||||
import {
|
||||
CancelablePromise,
|
||||
ignoreCancel,
|
||||
makeCancelable
|
||||
} from "../../util/CancelablePromise";
|
||||
import api from "../../util/api";
|
||||
import {
|
||||
Button,
|
||||
@@ -46,12 +50,13 @@ class DHCPInfo extends Component<WithNamespaces, DHCPInfoState> {
|
||||
router_ip: "",
|
||||
lease_time: 0,
|
||||
domain: "",
|
||||
ipv6_support: false
|
||||
ipv6_support: false,
|
||||
rapid_commit: true
|
||||
}
|
||||
};
|
||||
|
||||
private loadHandler: undefined | CancelablePromise<ApiDhcpSettings>;
|
||||
private updateHandler: undefined | CancelablePromise<ApiResultResponse>;
|
||||
private updateHandler: undefined | CancelablePromise<ApiSuccessResponse>;
|
||||
|
||||
loadDHCPInfo = () => {
|
||||
this.loadHandler = makeCancelable(api.getDHCPInfo());
|
||||
@@ -298,6 +303,17 @@ class DHCPInfo extends Component<WithNamespaces, DHCPInfoState> {
|
||||
{t("IPv6 Support")}
|
||||
</Label>
|
||||
</FormGroup>
|
||||
<FormGroup check>
|
||||
<Label check>
|
||||
<Input
|
||||
type="checkbox"
|
||||
disabled={!this.state.settings.active}
|
||||
checked={this.state.settings.rapid_commit}
|
||||
onChange={this.onChange("rapid_commit", "checked")}
|
||||
/>
|
||||
{t("Rapid Commit")}
|
||||
</Label>
|
||||
</FormGroup>
|
||||
<Button
|
||||
type="submit"
|
||||
disabled={
|
||||
|
||||
@@ -10,7 +10,11 @@
|
||||
|
||||
import React, { Component, FormEvent } from "react";
|
||||
import { WithNamespaces, withNamespaces } from "react-i18next";
|
||||
import { CancelablePromise, ignoreCancel, makeCancelable } from "../../util";
|
||||
import {
|
||||
CancelablePromise,
|
||||
ignoreCancel,
|
||||
makeCancelable
|
||||
} from "../../util/CancelablePromise";
|
||||
import api from "../../util/api";
|
||||
import DnsList from "./DnsList";
|
||||
import { Button, Col, Form, FormGroup } from "reactstrap";
|
||||
@@ -52,7 +56,7 @@ class DNSInfo extends Component<WithNamespaces, DNSInfoState> {
|
||||
};
|
||||
|
||||
private loadHandler: undefined | CancelablePromise<ApiDnsSettings>;
|
||||
private updateHandler: undefined | CancelablePromise<ApiResultResponse>;
|
||||
private updateHandler: undefined | CancelablePromise<ApiSuccessResponse>;
|
||||
|
||||
loadDNSInfo = () => {
|
||||
this.loadHandler = makeCancelable(api.getDNSInfo());
|
||||
|
||||
@@ -10,7 +10,11 @@
|
||||
|
||||
import React, { ChangeEvent, Component, FormEvent } from "react";
|
||||
import { WithNamespaces, withNamespaces } from "react-i18next";
|
||||
import { CancelablePromise, ignoreCancel, makeCancelable } from "../../util";
|
||||
import {
|
||||
CancelablePromise,
|
||||
ignoreCancel,
|
||||
makeCancelable
|
||||
} from "../../util/CancelablePromise";
|
||||
import api from "../../util/api";
|
||||
import Alert, { AlertType } from "../common/Alert";
|
||||
import { Button, Col, Form, FormGroup, Input, Label } from "reactstrap";
|
||||
@@ -48,7 +52,7 @@ class PreferenceSettings extends Component<
|
||||
};
|
||||
|
||||
private loadHandler: undefined | CancelablePromise<ApiPreferences>;
|
||||
private updateHandler: undefined | CancelablePromise<ApiResultResponse>;
|
||||
private updateHandler: undefined | CancelablePromise<ApiSuccessResponse>;
|
||||
|
||||
loadPreferences = () => {
|
||||
this.loadHandler = makeCancelable(api.getPreferences());
|
||||
|
||||
@@ -24,7 +24,8 @@ const fakeData = {
|
||||
router_ip: "192.168.1.1",
|
||||
lease_time: "24",
|
||||
domain: "lan",
|
||||
ipv6_support: false
|
||||
ipv6_support: false,
|
||||
rapid_commit: true
|
||||
};
|
||||
|
||||
it("retrieves settings correctly", async () => {
|
||||
|
||||
@@ -12,5 +12,6 @@ import { Config } from "./config";
|
||||
|
||||
export default {
|
||||
developmentMode: true,
|
||||
fakeAPI: false
|
||||
fakeAPI: false,
|
||||
apiPath: process.env.PUBLIC_URL + "/api"
|
||||
} as Config;
|
||||
|
||||
@@ -12,5 +12,6 @@ import { Config } from "./config";
|
||||
|
||||
export default {
|
||||
developmentMode: false,
|
||||
fakeAPI: false
|
||||
fakeAPI: false,
|
||||
apiPath: process.env.PUBLIC_URL + "/api"
|
||||
} as Config;
|
||||
|
||||
@@ -14,6 +14,7 @@ import productionConfig from "./config.production";
|
||||
export interface Config {
|
||||
developmentMode: boolean;
|
||||
fakeAPI: boolean;
|
||||
apiPath: string;
|
||||
}
|
||||
|
||||
let config: Config;
|
||||
@@ -26,6 +27,7 @@ if (process.env.NODE_ENV === "development") {
|
||||
|
||||
if (process.env.REACT_APP_FAKE_API) {
|
||||
config.fakeAPI = true;
|
||||
config.apiPath = process.env.PUBLIC_URL + "/fakeAPI";
|
||||
}
|
||||
|
||||
export default config;
|
||||
|
||||
+1
-1
@@ -19,7 +19,7 @@ import "./scss/style.css";
|
||||
import Full from "./containers/Full";
|
||||
import api from "./util/api";
|
||||
import { setupI18n } from "./util/i18n";
|
||||
import { getBasePath } from "./util";
|
||||
import { getBasePath } from "./util/basePath";
|
||||
|
||||
// Before rendering anything, check if there is a session cookie.
|
||||
// Note: the user could have an old session, so the first API call
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
// Variable overrides
|
||||
$border-color: #0275d8;
|
||||
$navbar-bg: #3c8dbc;
|
||||
$navbar-brand-bg: #367fa9;
|
||||
$navbar-brand-width: 200px;
|
||||
|
||||
Vendored
+1
@@ -67,6 +67,7 @@ interface ApiDhcpSettings {
|
||||
lease_time: number;
|
||||
domain: string;
|
||||
ipv6_support: boolean;
|
||||
rapid_commit: boolean;
|
||||
}
|
||||
|
||||
interface ApiFtlDbResponse {
|
||||
|
||||
@@ -3,55 +3,22 @@
|
||||
* Network-wide ad blocking via your own hardware.
|
||||
*
|
||||
* Web Interface
|
||||
* Various utilities
|
||||
* Wrap promises to make them cancelable
|
||||
*
|
||||
* This file is copyright under the latest version of the EUPL.
|
||||
* Please see LICENSE file for your rights under this license. */
|
||||
|
||||
import { TimeRange } from "../components/common/context/TimeRangeContext";
|
||||
|
||||
/**
|
||||
* Pad a two digit number
|
||||
*
|
||||
* @param num the number
|
||||
* @returns A padding number string
|
||||
* A promise which can be canceled
|
||||
*/
|
||||
export const padNumber = (num: number): string => {
|
||||
return ("00" + num).substr(-2, 2);
|
||||
};
|
||||
|
||||
/**
|
||||
* Get the base path of the web interface. The API will inject a base element
|
||||
* for this purpose, but if the web interface is not hosted by the API, it will
|
||||
* fall back to the public URL set by Create React App.
|
||||
*
|
||||
* @returns The base path to use
|
||||
*/
|
||||
export const getBasePath = (): string => {
|
||||
const baseElement = document.getElementsByTagName("base")[0];
|
||||
|
||||
if (baseElement) {
|
||||
return new URL(baseElement.href).pathname;
|
||||
} else {
|
||||
return process.env.PUBLIC_URL;
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Dynamically calculate a time interval so there are always 144 data points
|
||||
* (144 so that every point represents 10 minutes when the range is 24 hours)
|
||||
*
|
||||
* @param range The range to find the interval for
|
||||
*/
|
||||
export const getIntervalForRange = (range: TimeRange): number => {
|
||||
return Math.ceil((range.until.unix() - range.from.unix()) / 144);
|
||||
};
|
||||
|
||||
export interface CancelablePromise<T> {
|
||||
promise: Promise<T>;
|
||||
cancel: () => void;
|
||||
}
|
||||
|
||||
/**
|
||||
* The options given to {@link makeCancelable}
|
||||
*/
|
||||
export interface CancelableOptions {
|
||||
/**
|
||||
* The function to call to repeat the promise
|
||||
@@ -64,13 +31,19 @@ export interface CancelableOptions {
|
||||
interval: number;
|
||||
}
|
||||
|
||||
/**
|
||||
* The error thrown when the {@link CancelablePromise} is canceled
|
||||
*/
|
||||
export interface CanceledError {
|
||||
isCanceled: true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Make a promise cancelable and repeatable
|
||||
*
|
||||
* @param promise the promise
|
||||
* @param options the interval repeat options
|
||||
* @returns {{promise: Promise<T>, cancel(): void}} a handle on the cancelable
|
||||
* promise
|
||||
* @returns a cancelable promise
|
||||
*/
|
||||
export function makeCancelable<T>(
|
||||
promise: Promise<T>,
|
||||
@@ -80,17 +53,24 @@ export function makeCancelable<T>(
|
||||
let repeatId: NodeJS.Timeout | null = null;
|
||||
|
||||
const handle = (
|
||||
resolve: (value: any) => void,
|
||||
resolve: (value: T) => void,
|
||||
reject: (error: any) => void,
|
||||
val: T,
|
||||
isError: boolean
|
||||
) => {
|
||||
if (hasCanceled) reject({ isCanceled: true });
|
||||
else {
|
||||
if (isError) reject(val);
|
||||
else resolve(val);
|
||||
if (hasCanceled) {
|
||||
reject({ isCanceled: true });
|
||||
return;
|
||||
}
|
||||
|
||||
if (options) repeatId = setTimeout(options.repeat, options.interval);
|
||||
if (isError) {
|
||||
reject(val);
|
||||
} else {
|
||||
resolve(val);
|
||||
}
|
||||
|
||||
if (options) {
|
||||
repeatId = setTimeout(options.repeat, options.interval);
|
||||
}
|
||||
};
|
||||
|
||||
@@ -0,0 +1,102 @@
|
||||
/* Pi-hole: A black hole for Internet advertisements
|
||||
* (c) 2019 Pi-hole, LLC (https://pi-hole.net)
|
||||
* Network-wide ad blocking via your own hardware.
|
||||
*
|
||||
* Web Interface
|
||||
* Tests for canceling promises
|
||||
*
|
||||
* This file is copyright under the latest version of the EUPL.
|
||||
* Please see LICENSE file for your rights under this license. */
|
||||
|
||||
import { makeCancelable, ignoreCancel } from "../CancelablePromise";
|
||||
|
||||
describe("makeCancelable", () => {
|
||||
const testValue = "test";
|
||||
const testError = "testError";
|
||||
const promise = Promise.resolve(testValue);
|
||||
const promiseErr = Promise.reject(testError);
|
||||
|
||||
it("passes through promises in the default case", async () => {
|
||||
const cancelablePromise = makeCancelable(promise);
|
||||
|
||||
await expect(cancelablePromise.promise).resolves.toEqual(testValue);
|
||||
});
|
||||
|
||||
it("passes through errors in the default case", async () => {
|
||||
const cancelablePromise = makeCancelable(promiseErr);
|
||||
|
||||
await expect(cancelablePromise.promise).rejects.toEqual(testError);
|
||||
});
|
||||
|
||||
it("rejects with cancel error if canceled", async () => {
|
||||
const cancelablePromise = makeCancelable(promise);
|
||||
|
||||
cancelablePromise.cancel();
|
||||
|
||||
await expect(cancelablePromise.promise).rejects.toEqual({
|
||||
isCanceled: true
|
||||
});
|
||||
});
|
||||
|
||||
it("calls the repeat function after resolving", async () => {
|
||||
jest.useFakeTimers();
|
||||
|
||||
const mockFunction = jest.fn();
|
||||
const interval = 1000;
|
||||
const cancelablePromise = makeCancelable(promise, {
|
||||
interval,
|
||||
repeat: mockFunction
|
||||
});
|
||||
|
||||
await expect(cancelablePromise.promise).resolves;
|
||||
|
||||
expect(setTimeout).toHaveBeenCalledWith(mockFunction, interval);
|
||||
});
|
||||
|
||||
it("calls the repeat function after rejecting", async () => {
|
||||
jest.useFakeTimers();
|
||||
|
||||
const mockFunction = jest.fn();
|
||||
const interval = 1000;
|
||||
const cancelablePromise = makeCancelable(promiseErr, {
|
||||
interval,
|
||||
repeat: mockFunction
|
||||
});
|
||||
|
||||
await expect(cancelablePromise.promise).rejects.toEqual(testError);
|
||||
|
||||
expect(setTimeout).toHaveBeenCalledWith(mockFunction, interval);
|
||||
});
|
||||
|
||||
it("clears the timeout if canceled after resolving", async () => {
|
||||
jest.useFakeTimers();
|
||||
|
||||
const mockFunction = jest.fn();
|
||||
const interval = 1000;
|
||||
const cancelablePromise = makeCancelable(promise, {
|
||||
interval,
|
||||
repeat: mockFunction
|
||||
});
|
||||
|
||||
await cancelablePromise.promise;
|
||||
|
||||
cancelablePromise.cancel();
|
||||
|
||||
expect(clearTimeout).toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
describe("ignoreCancel", () => {
|
||||
it("passes through non-canceled errors", async () => {
|
||||
const testError = "test";
|
||||
const promise = Promise.reject(testError);
|
||||
|
||||
await expect(promise.catch(ignoreCancel)).rejects.toEqual(testError);
|
||||
});
|
||||
|
||||
it("does not pass through canceled errors", async () => {
|
||||
const canceledPromise = Promise.reject({ isCanceled: true });
|
||||
|
||||
await expect(canceledPromise.catch(ignoreCancel)).resolves;
|
||||
});
|
||||
});
|
||||
+311
-10
@@ -3,21 +3,322 @@
|
||||
* Network-wide ad blocking via your own hardware.
|
||||
*
|
||||
* Web Interface
|
||||
* Utility function tests
|
||||
* API service tests
|
||||
*
|
||||
* 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 { ApiClient } from "../api";
|
||||
import HttpClient from "../http";
|
||||
import { TimeRange } from "../../components/common/context/TimeRangeContext";
|
||||
import moment from "moment";
|
||||
import { Config } from "../../config";
|
||||
|
||||
// This is a dumb test used to set up the next test,
|
||||
// which checks that the logged in state is reset before each test
|
||||
it("sets logged in to true", () => {
|
||||
api.loggedIn = true;
|
||||
// Test each endpoint function to make sure it is calling the right endpoint
|
||||
// with the right data.
|
||||
describe("ApiClient", () => {
|
||||
// Services
|
||||
let httpClient: HttpClient;
|
||||
let api: ApiClient;
|
||||
|
||||
expect(api.loggedIn).toBeTruthy();
|
||||
});
|
||||
// Test response data
|
||||
const getData = { test: "GET" };
|
||||
const postData = { test: "POST" };
|
||||
const putData = { test: "PUT" };
|
||||
const deleteData = { test: "DELETE" };
|
||||
const getPromise = Promise.resolve(getData);
|
||||
const postPromise = Promise.resolve(postData);
|
||||
const putPromise = Promise.resolve(putData);
|
||||
const deletePromise = Promise.resolve(deleteData);
|
||||
|
||||
it("resets the logged in state for each test", () => {
|
||||
expect(api.loggedIn).toBeFalsy();
|
||||
// Test data
|
||||
const range: TimeRange = {
|
||||
name: "Test time range",
|
||||
from: moment("2019-04-12T01:03:17+00:00"),
|
||||
until: moment("2019-04-13T01:03:17+00:00")
|
||||
};
|
||||
const rangeParams = "from=1555030997&until=1555117397";
|
||||
const config: Config = {
|
||||
developmentMode: true,
|
||||
fakeAPI: true,
|
||||
apiPath: "/admin/api"
|
||||
};
|
||||
|
||||
beforeEach(() => {
|
||||
// Create fresh services
|
||||
httpClient = ({
|
||||
get: jest.fn(() => getPromise),
|
||||
post: jest.fn(() => postPromise),
|
||||
put: jest.fn(() => putPromise),
|
||||
delete: jest.fn(() => deletePromise),
|
||||
config
|
||||
} as any) as HttpClient;
|
||||
api = new ApiClient(httpClient);
|
||||
});
|
||||
|
||||
describe("authentication calls", () => {
|
||||
it("should call login endpoint with auth headers", async () => {
|
||||
const key = "test";
|
||||
await expect(api.authenticate(key)).resolves.toEqual(getData);
|
||||
expect(httpClient.get).toHaveBeenCalledWith("auth", {
|
||||
headers: { "X-Pi-hole-Authenticate": key }
|
||||
});
|
||||
});
|
||||
|
||||
it("should call logout endpoint", async () => {
|
||||
await expect(api.logout()).resolves.toEqual(deleteData);
|
||||
expect(httpClient.delete).toHaveBeenCalledWith("auth");
|
||||
});
|
||||
});
|
||||
|
||||
describe("statistics calls", () => {
|
||||
it("should call summary endpoint", async () => {
|
||||
await expect(api.getSummary()).resolves.toEqual(getData);
|
||||
expect(httpClient.get).toHaveBeenCalledWith("stats/summary");
|
||||
});
|
||||
|
||||
it("should call history graph endpoint", async () => {
|
||||
await expect(api.getHistoryGraph()).resolves.toEqual(getData);
|
||||
expect(httpClient.get).toHaveBeenCalledWith("stats/overTime/history");
|
||||
});
|
||||
|
||||
it("should call clients graph endpoint", async () => {
|
||||
await expect(api.getClientsGraph()).resolves.toEqual(getData);
|
||||
expect(httpClient.get).toHaveBeenCalledWith("stats/overTime/clients");
|
||||
});
|
||||
|
||||
it("should call query types endpoint", async () => {
|
||||
await expect(api.getQueryTypes()).resolves.toEqual(getData);
|
||||
expect(httpClient.get).toHaveBeenCalledWith("stats/query_types");
|
||||
});
|
||||
|
||||
it("should call upstreams endpoint", async () => {
|
||||
await expect(api.getUpstreams()).resolves.toEqual(getData);
|
||||
expect(httpClient.get).toHaveBeenCalledWith("stats/upstreams");
|
||||
});
|
||||
|
||||
it("should call top domains endpoint", async () => {
|
||||
await expect(api.getTopDomains()).resolves.toEqual(getData);
|
||||
expect(httpClient.get).toHaveBeenCalledWith("stats/top_domains");
|
||||
});
|
||||
|
||||
it("should call top blocked endpoint (top_domains?blocked=true)", async () => {
|
||||
config.fakeAPI = false;
|
||||
await expect(api.getTopBlocked()).resolves.toEqual(getData);
|
||||
expect(httpClient.get).toHaveBeenCalledWith(
|
||||
"stats/top_domains?blocked=true"
|
||||
);
|
||||
});
|
||||
|
||||
it("should call top blocked endpoint (top_blocked)", async () => {
|
||||
config.fakeAPI = true;
|
||||
await expect(api.getTopBlocked()).resolves.toEqual(getData);
|
||||
expect(httpClient.get).toHaveBeenCalledWith("stats/top_blocked");
|
||||
});
|
||||
|
||||
it("should call top clients endpoint", async () => {
|
||||
await expect(api.getTopClients()).resolves.toEqual(getData);
|
||||
expect(httpClient.get).toHaveBeenCalledWith("stats/top_clients");
|
||||
});
|
||||
|
||||
it("should call top history endpoint with params", async () => {
|
||||
const params = { test: "params" };
|
||||
await expect(api.getHistory(params)).resolves.toEqual(getData);
|
||||
expect(httpClient.get).toHaveBeenCalledWith("stats/history?test=params");
|
||||
});
|
||||
});
|
||||
|
||||
describe("database statistic calls", () => {
|
||||
it("should call summary DB endpoint with time range", async () => {
|
||||
await expect(api.getSummaryDb(range)).resolves.toEqual(getData);
|
||||
expect(httpClient.get).toHaveBeenCalledWith(
|
||||
"stats/database/summary?" + rangeParams
|
||||
);
|
||||
});
|
||||
|
||||
it("should call history graph DB endpoint with interval and time range", async () => {
|
||||
await expect(api.getHistoryGraphDb(range, 100)).resolves.toEqual(getData);
|
||||
expect(httpClient.get).toHaveBeenCalledWith(
|
||||
"stats/database/overTime/history?interval=100&" + rangeParams
|
||||
);
|
||||
});
|
||||
|
||||
it("should call client graph DB endpoint with interval and time range", async () => {
|
||||
await expect(api.getClientsGraphDb(range, 100)).resolves.toEqual(getData);
|
||||
expect(httpClient.get).toHaveBeenCalledWith(
|
||||
"stats/database/overTime/clients?interval=100&" + rangeParams
|
||||
);
|
||||
});
|
||||
|
||||
it("should call query types DB endpoint with time range", async () => {
|
||||
await expect(api.getQueryTypesDb(range)).resolves.toEqual(getData);
|
||||
expect(httpClient.get).toHaveBeenCalledWith(
|
||||
"stats/database/query_types?" + rangeParams
|
||||
);
|
||||
});
|
||||
|
||||
it("should call query types DB endpoint with time range", async () => {
|
||||
await expect(api.getQueryTypesDb(range)).resolves.toEqual(getData);
|
||||
expect(httpClient.get).toHaveBeenCalledWith(
|
||||
"stats/database/query_types?" + rangeParams
|
||||
);
|
||||
});
|
||||
|
||||
it("should call upstreams DB endpoint with time range", async () => {
|
||||
await expect(api.getUpstreamsDb(range)).resolves.toEqual(getData);
|
||||
expect(httpClient.get).toHaveBeenCalledWith(
|
||||
"stats/database/upstreams?" + rangeParams
|
||||
);
|
||||
});
|
||||
|
||||
it("should call top domains DB endpoint with time range", async () => {
|
||||
await expect(api.getTopDomainsDb(range)).resolves.toEqual(getData);
|
||||
expect(httpClient.get).toHaveBeenCalledWith(
|
||||
"stats/database/top_domains?" + rangeParams
|
||||
);
|
||||
});
|
||||
|
||||
it("should call top blocked DB endpoint (top_domains?blocked=true) with time range", async () => {
|
||||
config.fakeAPI = false;
|
||||
await expect(api.getTopBlockedDb(range)).resolves.toEqual(getData);
|
||||
expect(httpClient.get).toHaveBeenCalledWith(
|
||||
"stats/database/top_domains?blocked=true&" + rangeParams
|
||||
);
|
||||
});
|
||||
|
||||
it("should call top blocked DB endpoint (top_blocked) with time range", async () => {
|
||||
config.fakeAPI = true;
|
||||
await expect(api.getTopBlockedDb(range)).resolves.toEqual(getData);
|
||||
expect(httpClient.get).toHaveBeenCalledWith(
|
||||
"stats/database/top_blocked?" + rangeParams
|
||||
);
|
||||
});
|
||||
|
||||
it("should call top clients DB endpoint with time range", async () => {
|
||||
await expect(api.getTopClientsDb(range)).resolves.toEqual(getData);
|
||||
expect(httpClient.get).toHaveBeenCalledWith(
|
||||
"stats/database/top_clients?" + rangeParams
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe("dns calls", () => {
|
||||
it("should call get whitelist endpoint", async () => {
|
||||
await expect(api.getWhitelist()).resolves.toEqual(getData);
|
||||
expect(httpClient.get).toHaveBeenCalledWith("dns/whitelist");
|
||||
});
|
||||
|
||||
it("should call get blacklist endpoint", async () => {
|
||||
await expect(api.getBlacklist()).resolves.toEqual(getData);
|
||||
expect(httpClient.get).toHaveBeenCalledWith("dns/blacklist");
|
||||
});
|
||||
|
||||
it("should call get regexlist endpoint", async () => {
|
||||
await expect(api.getRegexlist()).resolves.toEqual(getData);
|
||||
expect(httpClient.get).toHaveBeenCalledWith("dns/regexlist");
|
||||
});
|
||||
|
||||
it("should call add whitelist endpoint with domain", async () => {
|
||||
const domain = "test.com";
|
||||
await expect(api.addWhitelist(domain)).resolves.toEqual(postData);
|
||||
expect(httpClient.post).toHaveBeenCalledWith("dns/whitelist", { domain });
|
||||
});
|
||||
|
||||
it("should call add blacklist endpoint with domain", async () => {
|
||||
const domain = "test.com";
|
||||
await expect(api.addBlacklist(domain)).resolves.toEqual(postData);
|
||||
expect(httpClient.post).toHaveBeenCalledWith("dns/blacklist", { domain });
|
||||
});
|
||||
|
||||
it("should call add regexlist endpoint with domain", async () => {
|
||||
const domain = "test.com";
|
||||
await expect(api.addRegexlist(domain)).resolves.toEqual(postData);
|
||||
expect(httpClient.post).toHaveBeenCalledWith("dns/regexlist", { domain });
|
||||
});
|
||||
|
||||
it("should call remove whitelist endpoint with domain", async () => {
|
||||
const domain = "test.com";
|
||||
await expect(api.removeWhitelist(domain)).resolves.toEqual(deleteData);
|
||||
expect(httpClient.delete).toHaveBeenCalledWith("dns/whitelist/" + domain);
|
||||
});
|
||||
|
||||
it("should call remove blacklist endpoint with domain", async () => {
|
||||
const domain = "test.com";
|
||||
await expect(api.removeBlacklist(domain)).resolves.toEqual(deleteData);
|
||||
expect(httpClient.delete).toHaveBeenCalledWith("dns/blacklist/" + domain);
|
||||
});
|
||||
|
||||
it("should call remove regexlist endpoint with encoded domain", async () => {
|
||||
const regex = "^test\\.com$";
|
||||
await expect(api.removeRegexlist(regex)).resolves.toEqual(deleteData);
|
||||
expect(httpClient.delete).toHaveBeenCalledWith(
|
||||
"dns/regexlist/%5Etest%5C.com%24"
|
||||
);
|
||||
});
|
||||
|
||||
it("should call get status endpoint", async () => {
|
||||
await expect(api.getStatus()).resolves.toEqual(getData);
|
||||
expect(httpClient.get).toHaveBeenCalledWith("dns/status");
|
||||
});
|
||||
|
||||
it("should call set status endpoint with action data", async () => {
|
||||
const action: StatusAction = "enable";
|
||||
const time = 100;
|
||||
await expect(api.setStatus(action, time)).resolves.toEqual(postData);
|
||||
expect(httpClient.post).toHaveBeenCalledWith("dns/status", {
|
||||
action,
|
||||
time
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe("settings calls", () => {
|
||||
it("should call get network settings endpoint", async () => {
|
||||
await expect(api.getNetworkInfo()).resolves.toEqual(getData);
|
||||
expect(httpClient.get).toHaveBeenCalledWith("settings/network");
|
||||
});
|
||||
|
||||
it("should call version endpoint", async () => {
|
||||
await expect(api.getVersion()).resolves.toEqual(getData);
|
||||
expect(httpClient.get).toHaveBeenCalledWith("version");
|
||||
});
|
||||
|
||||
it("should call FTL DB settings endpoint", async () => {
|
||||
await expect(api.getFTLdb()).resolves.toEqual(getData);
|
||||
expect(httpClient.get).toHaveBeenCalledWith("settings/ftldb");
|
||||
});
|
||||
|
||||
it("should call get DNS settings endpoint", async () => {
|
||||
await expect(api.getDNSInfo()).resolves.toEqual(getData);
|
||||
expect(httpClient.get).toHaveBeenCalledWith("settings/dns");
|
||||
});
|
||||
|
||||
it("should call get DHCP settings endpoint", async () => {
|
||||
await expect(api.getDHCPInfo()).resolves.toEqual(getData);
|
||||
expect(httpClient.get).toHaveBeenCalledWith("settings/dhcp");
|
||||
});
|
||||
|
||||
it("should call get web preferences settings endpoint", async () => {
|
||||
await expect(api.getPreferences()).resolves.toEqual(getData);
|
||||
expect(httpClient.get).toHaveBeenCalledWith("settings/web");
|
||||
});
|
||||
|
||||
it("should call set DNS settings endpoint", async () => {
|
||||
const settings = ({ test: true } as any) as ApiDnsSettings;
|
||||
await expect(api.updateDNSInfo(settings)).resolves.toEqual(putData);
|
||||
expect(httpClient.put).toHaveBeenCalledWith("settings/dns", settings);
|
||||
});
|
||||
|
||||
it("should call set DHCP settings endpoint", async () => {
|
||||
const settings = ({ test: true } as any) as ApiDhcpSettings;
|
||||
await expect(api.updateDHCPInfo(settings)).resolves.toEqual(putData);
|
||||
expect(httpClient.put).toHaveBeenCalledWith("settings/dhcp", settings);
|
||||
});
|
||||
|
||||
it("should call set web preferences settings endpoint", async () => {
|
||||
const settings = ({ test: true } as any) as ApiPreferences;
|
||||
await expect(api.updatePreferences(settings)).resolves.toEqual(putData);
|
||||
expect(httpClient.put).toHaveBeenCalledWith("settings/web", settings);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -0,0 +1,31 @@
|
||||
/* Pi-hole: A black hole for Internet advertisements
|
||||
* (c) 2019 Pi-hole, LLC (https://pi-hole.net)
|
||||
* Network-wide ad blocking via your own hardware.
|
||||
*
|
||||
* Web Interface
|
||||
* Tests for base path function
|
||||
*
|
||||
* This file is copyright under the latest version of the EUPL.
|
||||
* Please see LICENSE file for your rights under this license. */
|
||||
|
||||
import { getBasePath } from "../basePath";
|
||||
|
||||
it("should return the public URL when there is no base", () => {
|
||||
const publicUrl = process.env.PUBLIC_URL;
|
||||
|
||||
const basePath = getBasePath();
|
||||
|
||||
expect(basePath).toEqual(publicUrl);
|
||||
});
|
||||
|
||||
it("should return the path from the base element when it exists", () => {
|
||||
const expectedBasePath = "/admin";
|
||||
const baseElement = document.createElement("base");
|
||||
baseElement.href = expectedBasePath;
|
||||
document.head.appendChild(baseElement);
|
||||
|
||||
const actualBasePath = getBasePath();
|
||||
|
||||
document.head.removeChild(baseElement);
|
||||
expect(actualBasePath).toEqual(expectedBasePath);
|
||||
});
|
||||
@@ -0,0 +1,51 @@
|
||||
/* Pi-hole: A black hole for Internet advertisements
|
||||
* (c) 2019 Pi-hole, LLC (https://pi-hole.net)
|
||||
* Network-wide ad blocking via your own hardware.
|
||||
*
|
||||
* Web Interface
|
||||
* Graph utility tests
|
||||
*
|
||||
* This file is copyright under the latest version of the EUPL.
|
||||
* Please see LICENSE file for your rights under this license. */
|
||||
|
||||
import { padNumber, getIntervalForRange } from "../graphUtils";
|
||||
import { TimeRange } from "../../components/common/context/TimeRangeContext";
|
||||
import moment from "moment";
|
||||
|
||||
describe("padNumber", () => {
|
||||
it("pads 0 to 00", () => {
|
||||
expect(padNumber(0)).toEqual("00");
|
||||
});
|
||||
|
||||
it("pads 1 to 01", () => {
|
||||
expect(padNumber(1)).toEqual("01");
|
||||
});
|
||||
|
||||
it("pads 12 to 12", () => {
|
||||
expect(padNumber(12)).toEqual("12");
|
||||
});
|
||||
});
|
||||
|
||||
describe("getIntervalForRange", () => {
|
||||
it("returns 10 minutes for 24 hours", () => {
|
||||
const range: TimeRange = {
|
||||
name: "24 Hours",
|
||||
from: moment().subtract(1, "day"),
|
||||
until: moment()
|
||||
};
|
||||
|
||||
expect(getIntervalForRange(range)).toEqual(10 * 60);
|
||||
});
|
||||
|
||||
it("returns 1 day for 144 days", () => {
|
||||
const range: TimeRange = {
|
||||
name: "144 days",
|
||||
// Use seconds instead of days to ensure the difference in epoch time is
|
||||
// equal to 144 days
|
||||
from: moment().subtract(144 * 24 * 60 * 60, "seconds"),
|
||||
until: moment()
|
||||
};
|
||||
|
||||
expect(getIntervalForRange(range)).toEqual(24 * 60 * 60);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,288 @@
|
||||
/* Pi-hole: A black hole for Internet advertisements
|
||||
* (c) 2019 Pi-hole, LLC (https://pi-hole.net)
|
||||
* Network-wide ad blocking via your own hardware.
|
||||
*
|
||||
* Web Interface
|
||||
* Test basic HTTP functions
|
||||
*
|
||||
* This file is copyright under the latest version of the EUPL.
|
||||
* Please see LICENSE file for your rights under this license. */
|
||||
|
||||
import HttpClient, {
|
||||
checkForErrors,
|
||||
checkIfLoggedOut,
|
||||
convertJSON,
|
||||
paramsToString,
|
||||
timeRangeToParams
|
||||
} from "../http";
|
||||
import api from "../api";
|
||||
import { Config } from "../../config";
|
||||
import { CanceledError } from "../CancelablePromise";
|
||||
import { TimeRange } from "../../components/common/context/TimeRangeContext";
|
||||
import moment from "moment";
|
||||
import fetchMock from "fetch-mock";
|
||||
|
||||
const originalReload = window.location.reload;
|
||||
|
||||
/**
|
||||
* Mock window.location.reload
|
||||
* https://remarkablemark.org/blog/2018/11/17/mock-window-location/
|
||||
*
|
||||
* Restore it with {@link restoreLocationReload}
|
||||
*/
|
||||
const mockLocationReload = () => {
|
||||
Object.defineProperty(window.location, "reload", {
|
||||
configurable: true
|
||||
});
|
||||
|
||||
window.location.reload = jest.fn();
|
||||
};
|
||||
|
||||
/**
|
||||
* Restore window.location.reload after being mocked by
|
||||
* {@link mockLocationReload}
|
||||
*/
|
||||
const restoreLocationReload = () => {
|
||||
window.location.reload = originalReload;
|
||||
};
|
||||
|
||||
describe("HttpClient", () => {
|
||||
const testEndpoint = "test";
|
||||
const testEndpointFull = "/api/test";
|
||||
const data = { test: true };
|
||||
const config: Config = {
|
||||
developmentMode: true,
|
||||
apiPath: "/api",
|
||||
fakeAPI: false
|
||||
};
|
||||
const canceledError: CanceledError = { isCanceled: true };
|
||||
let httpClient: HttpClient;
|
||||
|
||||
beforeEach(() => {
|
||||
httpClient = new HttpClient(config);
|
||||
});
|
||||
|
||||
describe("handleResponse", () => {
|
||||
it("should make a GET request and return the parsed data", async () => {
|
||||
const response = {
|
||||
status: 200,
|
||||
json: () => Promise.resolve(data)
|
||||
} as Response;
|
||||
|
||||
await expect(httpClient.handleResponse(response)).resolves.toEqual(data);
|
||||
});
|
||||
|
||||
it("should cancel if logged out by API", async () => {
|
||||
const error: ApiError = {
|
||||
key: "unauthorized",
|
||||
message: "Unauthorized",
|
||||
data: null
|
||||
};
|
||||
const response = {
|
||||
status: 401,
|
||||
json: () => Promise.resolve({ error })
|
||||
} as Response;
|
||||
|
||||
api.loggedIn = true;
|
||||
mockLocationReload();
|
||||
|
||||
await expect(httpClient.handleResponse(response)).rejects.toEqual(
|
||||
canceledError
|
||||
);
|
||||
expect(window.location.reload).toHaveBeenCalled();
|
||||
restoreLocationReload();
|
||||
});
|
||||
|
||||
it("should reject with the API error if set", async () => {
|
||||
const error: ApiError = {
|
||||
key: "test_key",
|
||||
message: "Test message",
|
||||
data: null
|
||||
};
|
||||
const response = {
|
||||
status: 500,
|
||||
json: () => Promise.resolve({ error })
|
||||
} as Response;
|
||||
|
||||
await expect(httpClient.handleResponse(response)).rejects.toEqual(error);
|
||||
});
|
||||
});
|
||||
|
||||
describe("HTTP functions", () => {
|
||||
it("should make a GET request and call handleResponse", async () => {
|
||||
fetchMock.get(testEndpointFull, { body: data });
|
||||
// @ts-ignore
|
||||
httpClient.handleResponse = jest.fn(() => Promise.resolve(data));
|
||||
|
||||
await expect(httpClient.get(testEndpoint)).resolves.toEqual(data);
|
||||
const request = fetchMock.lastCall(testEndpointFull)![1]!;
|
||||
|
||||
expect(httpClient.handleResponse).toHaveBeenCalled();
|
||||
expect(request.method).toEqual("GET");
|
||||
});
|
||||
|
||||
it("should make a POST request and call handleResponse", async () => {
|
||||
fetchMock.post(testEndpointFull, { body: data });
|
||||
// @ts-ignore
|
||||
httpClient.handleResponse = jest.fn(() => Promise.resolve(data));
|
||||
|
||||
await expect(httpClient.post(testEndpoint, data)).resolves.toEqual(data);
|
||||
const request = fetchMock.lastCall(testEndpointFull)![1]!;
|
||||
|
||||
expect(httpClient.handleResponse).toHaveBeenCalled();
|
||||
expect(request.method).toEqual("POST");
|
||||
expect(request.body).toEqual(JSON.stringify(data));
|
||||
});
|
||||
|
||||
it("should make a PUT request and call handleResponse", async () => {
|
||||
fetchMock.put(testEndpointFull, { body: data });
|
||||
// @ts-ignore
|
||||
httpClient.handleResponse = jest.fn(() => Promise.resolve(data));
|
||||
|
||||
await expect(httpClient.put(testEndpoint, data)).resolves.toEqual(data);
|
||||
const request = fetchMock.lastCall(testEndpointFull)![1]!;
|
||||
|
||||
expect(httpClient.handleResponse).toHaveBeenCalled();
|
||||
expect(request.method).toEqual("PUT");
|
||||
expect(request.body).toEqual(JSON.stringify(data));
|
||||
});
|
||||
|
||||
it("should make a DELETE request and call handleResponse", async () => {
|
||||
fetchMock.delete(testEndpointFull, { body: data });
|
||||
// @ts-ignore
|
||||
httpClient.handleResponse = jest.fn(() => Promise.resolve(data));
|
||||
|
||||
await expect(httpClient.delete(testEndpoint)).resolves.toEqual(data);
|
||||
const request = fetchMock.lastCall(testEndpointFull)![1]!;
|
||||
|
||||
expect(httpClient.handleResponse).toHaveBeenCalled();
|
||||
expect(request.method).toEqual("DELETE");
|
||||
});
|
||||
});
|
||||
|
||||
describe("urlFor", () => {
|
||||
it("uses the fakeAPI route if configured", () => {
|
||||
const config: Config = {
|
||||
developmentMode: false,
|
||||
fakeAPI: true,
|
||||
apiPath: "/fakeAPI"
|
||||
};
|
||||
const httpClient = new HttpClient(config);
|
||||
|
||||
expect(httpClient.urlFor("test")).toEqual("/fakeAPI/test");
|
||||
});
|
||||
|
||||
it("uses the production route if configured", () => {
|
||||
const config: Config = {
|
||||
developmentMode: false,
|
||||
fakeAPI: false,
|
||||
apiPath: "/admin/api"
|
||||
};
|
||||
const httpClient = new HttpClient(config);
|
||||
|
||||
expect(httpClient.urlFor("test")).toEqual("/admin/api/test");
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe("checkIfLoggedOut", () => {
|
||||
it("should pass the response through if logged in and not a 401", async () => {
|
||||
const response = { status: 200 } as Response;
|
||||
api.loggedIn = true;
|
||||
|
||||
await expect(checkIfLoggedOut(response)).resolves.toEqual(response);
|
||||
});
|
||||
|
||||
it("should pass the response through if not logged in and not a 401", async () => {
|
||||
const response = { status: 200 } as Response;
|
||||
api.loggedIn = false;
|
||||
|
||||
await expect(checkIfLoggedOut(response)).resolves.toEqual(response);
|
||||
});
|
||||
|
||||
it("should pass the response through if not logged in and is a 401", async () => {
|
||||
const response = { status: 401 } as Response;
|
||||
api.loggedIn = false;
|
||||
|
||||
await expect(checkIfLoggedOut(response)).resolves.toEqual(response);
|
||||
});
|
||||
|
||||
it("should clear the session cookie and reload if logged in and response is a 401", async () => {
|
||||
const response = { status: 401 } as Response;
|
||||
api.loggedIn = true;
|
||||
document.cookie = "user_id=test";
|
||||
mockLocationReload();
|
||||
|
||||
await expect(checkIfLoggedOut(response)).rejects.toEqual({
|
||||
isCanceled: true
|
||||
});
|
||||
|
||||
expect(window.location.reload).toHaveBeenCalled();
|
||||
expect(document.cookie).toHaveLength(0);
|
||||
|
||||
restoreLocationReload();
|
||||
});
|
||||
});
|
||||
|
||||
describe("convertJSON", () => {
|
||||
it("should convert to JSON if it is not canceled or an error", async () => {
|
||||
const body = { test: true };
|
||||
const response = new Response(JSON.stringify(body));
|
||||
|
||||
await expect(convertJSON(response)).resolves.toEqual(body);
|
||||
});
|
||||
|
||||
it("should reject with input if canceled", async () => {
|
||||
const cancelError: CanceledError = {
|
||||
isCanceled: true
|
||||
};
|
||||
|
||||
await expect(convertJSON(cancelError)).rejects.toEqual(cancelError);
|
||||
});
|
||||
|
||||
it("should reject with input if error", async () => {
|
||||
const error = new Error("test");
|
||||
|
||||
await expect(convertJSON(error)).rejects.toEqual(error);
|
||||
});
|
||||
});
|
||||
|
||||
describe("checkForErrors", () => {
|
||||
it("should pass through the data if there is no error", async () => {
|
||||
const data = { test: true };
|
||||
|
||||
await expect(checkForErrors(data)).resolves.toEqual(data);
|
||||
});
|
||||
|
||||
it("should reject with the error if there is an error", async () => {
|
||||
const data = { error: { test: true } };
|
||||
|
||||
await expect(checkForErrors(data)).rejects.toEqual(data.error);
|
||||
});
|
||||
});
|
||||
|
||||
describe("paramsToString", () => {
|
||||
it("converts an object into parameters", () => {
|
||||
const object = {
|
||||
test1: "1",
|
||||
test2: "two",
|
||||
test3: 3
|
||||
};
|
||||
const expectedParams = "test1=1&test2=two&test3=3";
|
||||
|
||||
expect(paramsToString(object)).toEqual(expectedParams);
|
||||
});
|
||||
});
|
||||
|
||||
describe("timeRangeToParams", () => {
|
||||
it("converts a time range into parameters", () => {
|
||||
const range: TimeRange = {
|
||||
name: "Test time range",
|
||||
from: moment("2019-04-12T01:03:17+00:00"),
|
||||
until: moment("2019-04-13T01:03:17+00:00")
|
||||
};
|
||||
const expectedParams = "from=1555030997&until=1555117397";
|
||||
|
||||
expect(timeRangeToParams(range)).toEqual(expectedParams);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,32 @@
|
||||
/* Pi-hole: A black hole for Internet advertisements
|
||||
* (c) 2019 Pi-hole, LLC (https://pi-hole.net)
|
||||
* Network-wide ad blocking via your own hardware.
|
||||
*
|
||||
* Web Interface
|
||||
* Test internationalization setup
|
||||
*
|
||||
* This file is copyright under the latest version of the EUPL.
|
||||
* Please see LICENSE file for your rights under this license. */
|
||||
|
||||
import { setupI18n } from "../i18n";
|
||||
|
||||
// Mock react-i18next, as it does not properly export the i18next module during
|
||||
// testing.
|
||||
// https://github.com/i18next/react-i18next/issues/434
|
||||
jest.mock("react-i18next", () => ({
|
||||
reactI18nextModule: {
|
||||
type: "3rdParty",
|
||||
init: () => {}
|
||||
}
|
||||
}));
|
||||
|
||||
// languages.json is generated during a build or run, so it may not exist yet
|
||||
jest.mock("../../languages.json", () => [], { virtual: true });
|
||||
|
||||
it("configures i18n successfully", async () => {
|
||||
// Provide a mock ajax function to the XHR backend
|
||||
const fakeAjax = (url: any, options: any, callback: any) => callback("", {});
|
||||
|
||||
// Make sure i18n initializes without error
|
||||
await setupI18n(fakeAjax);
|
||||
});
|
||||
@@ -0,0 +1,47 @@
|
||||
/* Pi-hole: A black hole for Internet advertisements
|
||||
* (c) 2019 Pi-hole, LLC (https://pi-hole.net)
|
||||
* Network-wide ad blocking via your own hardware.
|
||||
*
|
||||
* Web Interface
|
||||
* Result class tests
|
||||
*
|
||||
* This file is copyright under the latest version of the EUPL.
|
||||
* Please see LICENSE file for your rights under this license. */
|
||||
|
||||
import { Ok, Err } from "../result";
|
||||
|
||||
describe("Ok", () => {
|
||||
const testValue = "test";
|
||||
const ok = new Ok(testValue);
|
||||
|
||||
it("knows it's Ok", () => {
|
||||
expect(ok.isOk()).toBe(true);
|
||||
expect(ok.isErr()).toBe(false);
|
||||
});
|
||||
|
||||
it("unwraps without error", () => {
|
||||
expect(ok.unwrap()).toEqual(testValue);
|
||||
});
|
||||
|
||||
it("throws an error on unwrapErr", () => {
|
||||
expect(() => ok.unwrapErr()).toThrow("unwrapErr on a Result.Ok");
|
||||
});
|
||||
});
|
||||
|
||||
describe("Err", () => {
|
||||
const testValue = "test";
|
||||
const err = new Err(testValue);
|
||||
|
||||
it("knows it's Err", () => {
|
||||
expect(err.isOk()).toBe(false);
|
||||
expect(err.isErr()).toBe(true);
|
||||
});
|
||||
|
||||
it("throws an error on unwrap", () => {
|
||||
expect(() => err.unwrap()).toThrow("unwrap on a Result.Err");
|
||||
});
|
||||
|
||||
it("does not throw an error on unwrapErr", () => {
|
||||
expect(err.unwrapErr()).toEqual(testValue);
|
||||
});
|
||||
});
|
||||
+182
-123
@@ -8,157 +8,216 @@
|
||||
* This file is copyright under the latest version of the EUPL.
|
||||
* Please see LICENSE file for your rights under this license. */
|
||||
|
||||
import http, { paramsToString, timeRangeToParams } from "./http";
|
||||
import HttpClient, { paramsToString, timeRangeToParams } from "./http";
|
||||
import config from "../config";
|
||||
import { TimeRange } from "../components/common/context/TimeRangeContext";
|
||||
|
||||
export default {
|
||||
loggedIn: false,
|
||||
authenticate(key: string) {
|
||||
return http.get("auth", {
|
||||
headers: new Headers({ "X-Pi-hole-Authenticate": key })
|
||||
export class ApiClient {
|
||||
public loggedIn = false;
|
||||
|
||||
constructor(private http: HttpClient) {}
|
||||
|
||||
authenticate = (key: string): Promise<ApiSuccessResponse> => {
|
||||
return this.http.get("auth", {
|
||||
headers: { "X-Pi-hole-Authenticate": key }
|
||||
});
|
||||
},
|
||||
logout() {
|
||||
return http.delete("auth");
|
||||
},
|
||||
getSummary(): Promise<ApiSummary> {
|
||||
return http.get("stats/summary");
|
||||
},
|
||||
getSummaryDb(range: TimeRange): Promise<ApiSummary> {
|
||||
return http.get("stats/database/summary?" + timeRangeToParams(range));
|
||||
},
|
||||
getHistoryGraph(): Promise<Array<ApiHistoryGraphItem>> {
|
||||
return http.get("stats/overTime/history");
|
||||
},
|
||||
getHistoryGraphDb(
|
||||
};
|
||||
|
||||
logout = (): Promise<ApiSuccessResponse> => {
|
||||
return this.http.delete("auth");
|
||||
};
|
||||
|
||||
getSummary = (): Promise<ApiSummary> => {
|
||||
return this.http.get("stats/summary");
|
||||
};
|
||||
|
||||
getSummaryDb = (range: TimeRange): Promise<ApiSummary> => {
|
||||
return this.http.get("stats/database/summary?" + timeRangeToParams(range));
|
||||
};
|
||||
|
||||
getHistoryGraph = (): Promise<Array<ApiHistoryGraphItem>> => {
|
||||
return this.http.get("stats/overTime/history");
|
||||
};
|
||||
|
||||
getHistoryGraphDb = (
|
||||
range: TimeRange,
|
||||
interval: number
|
||||
): Promise<Array<ApiHistoryGraphItem>> {
|
||||
return http.get(
|
||||
): Promise<Array<ApiHistoryGraphItem>> => {
|
||||
return this.http.get(
|
||||
"stats/database/overTime/history?interval=" +
|
||||
interval +
|
||||
"&" +
|
||||
timeRangeToParams(range)
|
||||
);
|
||||
},
|
||||
getClientsGraph(): Promise<ApiClientsGraph> {
|
||||
return http.get("stats/overTime/clients");
|
||||
},
|
||||
getClientsGraphDb(
|
||||
};
|
||||
|
||||
getClientsGraph = (): Promise<ApiClientsGraph> => {
|
||||
return this.http.get("stats/overTime/clients");
|
||||
};
|
||||
|
||||
getClientsGraphDb = (
|
||||
range: TimeRange,
|
||||
interval: number
|
||||
): Promise<ApiClientsGraph> {
|
||||
return http.get(
|
||||
): Promise<ApiClientsGraph> => {
|
||||
return this.http.get(
|
||||
"stats/database/overTime/clients?interval=" +
|
||||
interval +
|
||||
"&" +
|
||||
timeRangeToParams(range)
|
||||
);
|
||||
},
|
||||
getQueryTypes(): Promise<Array<ApiQueryType>> {
|
||||
return http.get("stats/query_types");
|
||||
},
|
||||
getQueryTypesDb(range: TimeRange): Promise<Array<ApiQueryType>> {
|
||||
return http.get("stats/database/query_types?" + timeRangeToParams(range));
|
||||
},
|
||||
getUpstreams(): Promise<ApiUpstreams> {
|
||||
return http.get("stats/upstreams");
|
||||
},
|
||||
getUpstreamsDb(range: TimeRange): Promise<ApiUpstreams> {
|
||||
return http.get("stats/database/upstreams?" + timeRangeToParams(range));
|
||||
},
|
||||
getTopDomains(): Promise<ApiTopDomains> {
|
||||
return http.get("stats/top_domains");
|
||||
},
|
||||
getTopDomainsDb(range: TimeRange): Promise<ApiTopDomains> {
|
||||
return http.get("stats/database/top_domains?" + timeRangeToParams(range));
|
||||
},
|
||||
};
|
||||
|
||||
getQueryTypes = (): Promise<Array<ApiQueryType>> => {
|
||||
return this.http.get("stats/query_types");
|
||||
};
|
||||
|
||||
getQueryTypesDb = (range: TimeRange): Promise<Array<ApiQueryType>> => {
|
||||
return this.http.get(
|
||||
"stats/database/query_types?" + timeRangeToParams(range)
|
||||
);
|
||||
};
|
||||
|
||||
getUpstreams = (): Promise<ApiUpstreams> => {
|
||||
return this.http.get("stats/upstreams");
|
||||
};
|
||||
|
||||
getUpstreamsDb = (range: TimeRange): Promise<ApiUpstreams> => {
|
||||
return this.http.get(
|
||||
"stats/database/upstreams?" + timeRangeToParams(range)
|
||||
);
|
||||
};
|
||||
|
||||
getTopDomains = (): Promise<ApiTopDomains> => {
|
||||
return this.http.get("stats/top_domains");
|
||||
};
|
||||
|
||||
getTopDomainsDb = (range: TimeRange): Promise<ApiTopDomains> => {
|
||||
return this.http.get(
|
||||
"stats/database/top_domains?" + timeRangeToParams(range)
|
||||
);
|
||||
};
|
||||
|
||||
getTopBlocked(): Promise<ApiTopBlocked> {
|
||||
// The API uses a GET parameter to differentiate top domains from top
|
||||
// blocked, but the fake API is not able to handle GET parameters right now.
|
||||
const url = config.fakeAPI
|
||||
const url = this.http.config.fakeAPI
|
||||
? "stats/top_blocked"
|
||||
: "stats/top_domains?blocked=true";
|
||||
|
||||
return http.get(url);
|
||||
},
|
||||
return this.http.get(url);
|
||||
}
|
||||
|
||||
getTopBlockedDb(range: TimeRange): Promise<ApiTopBlocked> {
|
||||
// The API uses a GET parameter to differentiate top domains from top
|
||||
// blocked, but the fake API is not able to handle GET parameters right now.
|
||||
const url = config.fakeAPI
|
||||
const url = this.http.config.fakeAPI
|
||||
? "stats/database/top_blocked?"
|
||||
: "stats/database/top_domains?blocked=true&";
|
||||
|
||||
return http.get(url + timeRangeToParams(range));
|
||||
},
|
||||
getTopClients(): Promise<ApiTopClients> {
|
||||
return http.get("stats/top_clients");
|
||||
},
|
||||
getTopClientsDb(range: TimeRange): Promise<ApiTopClients> {
|
||||
return http.get("stats/database/top_clients?" + timeRangeToParams(range));
|
||||
},
|
||||
getHistory(params: any): Promise<ApiHistoryResponse> {
|
||||
return http.get("stats/history?" + paramsToString(params));
|
||||
},
|
||||
getWhitelist() {
|
||||
return http.get("dns/whitelist");
|
||||
},
|
||||
getBlacklist() {
|
||||
return http.get("dns/blacklist");
|
||||
},
|
||||
getRegexlist() {
|
||||
return http.get("dns/regexlist");
|
||||
},
|
||||
addWhitelist(domain: string) {
|
||||
return http.post("dns/whitelist", { domain: domain });
|
||||
},
|
||||
addBlacklist(domain: string) {
|
||||
return http.post("dns/blacklist", { domain: domain });
|
||||
},
|
||||
addRegexlist(domain: string) {
|
||||
return http.post("dns/regexlist", { domain: domain });
|
||||
},
|
||||
removeWhitelist(domain: string) {
|
||||
return http.delete("dns/whitelist/" + domain);
|
||||
},
|
||||
removeBlacklist(domain: string) {
|
||||
return http.delete("dns/blacklist/" + domain);
|
||||
},
|
||||
removeRegexlist(domain: string) {
|
||||
return http.delete("dns/regexlist/" + encodeURIComponent(domain));
|
||||
},
|
||||
getStatus(): Promise<ApiStatus> {
|
||||
return http.get("dns/status");
|
||||
},
|
||||
setStatus(action: StatusAction, time?: number) {
|
||||
return http.post("dns/status", { action, time });
|
||||
},
|
||||
getNetworkInfo(): Promise<ApiNetworkSettings> {
|
||||
return http.get("settings/network");
|
||||
},
|
||||
getVersion(): Promise<ApiVersions> {
|
||||
return http.get("version");
|
||||
},
|
||||
getFTLdb(): Promise<ApiFtlDbResponse> {
|
||||
return http.get("settings/ftldb");
|
||||
},
|
||||
getDNSInfo(): Promise<ApiDnsSettings> {
|
||||
return http.get("settings/dns");
|
||||
},
|
||||
getDHCPInfo(): Promise<ApiDhcpSettings> {
|
||||
return http.get("settings/dhcp");
|
||||
},
|
||||
updateDHCPInfo(settings: ApiDhcpSettings) {
|
||||
return http.put("settings/dhcp", settings);
|
||||
},
|
||||
updateDNSInfo(settings: ApiDnsSettings) {
|
||||
return http.put("settings/dns", settings);
|
||||
},
|
||||
getPreferences(): Promise<ApiPreferences> {
|
||||
return http.get("settings/web");
|
||||
},
|
||||
updatePreferences(settings: ApiPreferences) {
|
||||
return http.put("settings/web", settings);
|
||||
return this.http.get(url + timeRangeToParams(range));
|
||||
}
|
||||
};
|
||||
|
||||
getTopClients = (): Promise<ApiTopClients> => {
|
||||
return this.http.get("stats/top_clients");
|
||||
};
|
||||
|
||||
getTopClientsDb = (range: TimeRange): Promise<ApiTopClients> => {
|
||||
return this.http.get(
|
||||
"stats/database/top_clients?" + timeRangeToParams(range)
|
||||
);
|
||||
};
|
||||
|
||||
getHistory = (params: any): Promise<ApiHistoryResponse> => {
|
||||
return this.http.get("stats/history?" + paramsToString(params));
|
||||
};
|
||||
|
||||
getWhitelist = (): Promise<Array<string>> => {
|
||||
return this.http.get("dns/whitelist");
|
||||
};
|
||||
|
||||
getBlacklist = (): Promise<Array<string>> => {
|
||||
return this.http.get("dns/blacklist");
|
||||
};
|
||||
|
||||
getRegexlist = (): Promise<Array<string>> => {
|
||||
return this.http.get("dns/regexlist");
|
||||
};
|
||||
|
||||
addWhitelist = (domain: string): Promise<ApiSuccessResponse> => {
|
||||
return this.http.post("dns/whitelist", { domain: domain });
|
||||
};
|
||||
|
||||
addBlacklist = (domain: string): Promise<ApiSuccessResponse> => {
|
||||
return this.http.post("dns/blacklist", { domain: domain });
|
||||
};
|
||||
|
||||
addRegexlist = (domain: string): Promise<ApiSuccessResponse> => {
|
||||
return this.http.post("dns/regexlist", { domain: domain });
|
||||
};
|
||||
|
||||
removeWhitelist = (domain: string): Promise<ApiSuccessResponse> => {
|
||||
return this.http.delete("dns/whitelist/" + domain);
|
||||
};
|
||||
|
||||
removeBlacklist = (domain: string): Promise<ApiSuccessResponse> => {
|
||||
return this.http.delete("dns/blacklist/" + domain);
|
||||
};
|
||||
|
||||
removeRegexlist = (domain: string): Promise<ApiSuccessResponse> => {
|
||||
return this.http.delete("dns/regexlist/" + encodeURIComponent(domain));
|
||||
};
|
||||
|
||||
getStatus = (): Promise<ApiStatus> => {
|
||||
return this.http.get("dns/status");
|
||||
};
|
||||
|
||||
setStatus = (
|
||||
action: StatusAction,
|
||||
time?: number
|
||||
): Promise<ApiSuccessResponse> => {
|
||||
return this.http.post("dns/status", {
|
||||
action,
|
||||
time
|
||||
});
|
||||
};
|
||||
|
||||
getNetworkInfo = (): Promise<ApiNetworkSettings> => {
|
||||
return this.http.get("settings/network");
|
||||
};
|
||||
|
||||
getVersion = (): Promise<ApiVersions> => {
|
||||
return this.http.get("version");
|
||||
};
|
||||
|
||||
getFTLdb = (): Promise<ApiFtlDbResponse> => {
|
||||
return this.http.get("settings/ftldb");
|
||||
};
|
||||
|
||||
getDNSInfo = (): Promise<ApiDnsSettings> => {
|
||||
return this.http.get("settings/dns");
|
||||
};
|
||||
|
||||
getDHCPInfo = (): Promise<ApiDhcpSettings> => {
|
||||
return this.http.get("settings/dhcp");
|
||||
};
|
||||
|
||||
updateDHCPInfo = (settings: ApiDhcpSettings): Promise<ApiSuccessResponse> => {
|
||||
return this.http.put("settings/dhcp", settings);
|
||||
};
|
||||
|
||||
updateDNSInfo = (settings: ApiDnsSettings): Promise<ApiSuccessResponse> => {
|
||||
return this.http.put("settings/dns", settings);
|
||||
};
|
||||
|
||||
getPreferences = (): Promise<ApiPreferences> => {
|
||||
return this.http.get("settings/web");
|
||||
};
|
||||
|
||||
updatePreferences = (
|
||||
settings: ApiPreferences
|
||||
): Promise<ApiSuccessResponse> => {
|
||||
return this.http.put("settings/web", settings);
|
||||
};
|
||||
}
|
||||
|
||||
export default new ApiClient(new HttpClient(config));
|
||||
|
||||
@@ -0,0 +1,26 @@
|
||||
/* Pi-hole: A black hole for Internet advertisements
|
||||
* (c) 2019 Pi-hole, LLC (https://pi-hole.net)
|
||||
* Network-wide ad blocking via your own hardware.
|
||||
*
|
||||
* Web Interface
|
||||
* Provide the base path for relative paths
|
||||
*
|
||||
* This file is copyright under the latest version of the EUPL.
|
||||
* Please see LICENSE file for your rights under this license. */
|
||||
|
||||
/**
|
||||
* Get the base path of the web interface. The API will inject a base element
|
||||
* for this purpose, but if the web interface is not hosted by the API, it will
|
||||
* fall back to the public URL set by Create React App.
|
||||
*
|
||||
* @returns The base path to use
|
||||
*/
|
||||
export const getBasePath = (): string => {
|
||||
const baseElement = document.getElementsByTagName("base")[0];
|
||||
|
||||
if (baseElement) {
|
||||
return new URL(baseElement.href).pathname;
|
||||
} else {
|
||||
return process.env.PUBLIC_URL;
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,31 @@
|
||||
/* Pi-hole: A black hole for Internet advertisements
|
||||
* (c) 2019 Pi-hole, LLC (https://pi-hole.net)
|
||||
* Network-wide ad blocking via your own hardware.
|
||||
*
|
||||
* Web Interface
|
||||
* Graph utility functions
|
||||
*
|
||||
* This file is copyright under the latest version of the EUPL.
|
||||
* Please see LICENSE file for your rights under this license. */
|
||||
|
||||
import { TimeRange } from "../components/common/context/TimeRangeContext";
|
||||
|
||||
/**
|
||||
* Pad a two digit number
|
||||
*
|
||||
* @param num the number
|
||||
* @returns {string} a padding number string
|
||||
*/
|
||||
export const padNumber = (num: number) => {
|
||||
return ("00" + num).substr(-2, 2);
|
||||
};
|
||||
|
||||
/**
|
||||
* Dynamically calculate a time interval so there are always 144 data points
|
||||
* (144 so that every point represents 10 minutes when the range is 24 hours)
|
||||
*
|
||||
* @param range The range to find the interval for
|
||||
*/
|
||||
export const getIntervalForRange = (range: TimeRange): number => {
|
||||
return Math.ceil((range.until.unix() - range.from.unix()) / 144);
|
||||
};
|
||||
+98
-95
@@ -9,98 +9,124 @@
|
||||
* Please see LICENSE file for your rights under this license. */
|
||||
|
||||
import api from "./api";
|
||||
import config from "../config";
|
||||
import { Config } from "../config";
|
||||
import { TimeRange } from "../components/common/context/TimeRangeContext";
|
||||
import { CanceledError } from "./CancelablePromise";
|
||||
|
||||
/**
|
||||
* A group of HTTP functions. Each function parses the response checks for
|
||||
* errors
|
||||
* A class which provides HTTP functions. Each function parses the response and
|
||||
* checks for errors
|
||||
*/
|
||||
export default {
|
||||
export default class HttpClient {
|
||||
constructor(public config: Config) {}
|
||||
|
||||
/**
|
||||
* Check if the user is logged out, convert to JSON, and check for API errors
|
||||
*
|
||||
* @param response The HTTP response
|
||||
*/
|
||||
handleResponse = <T extends any>(response: Response): Promise<T> => {
|
||||
// @ts-ignore
|
||||
return checkIfLoggedOut(response)
|
||||
.then(convertJSON)
|
||||
.then(checkForErrors);
|
||||
};
|
||||
|
||||
/**
|
||||
* Perform a GET request
|
||||
*
|
||||
* @param url the URL to access
|
||||
* @param options optional fetch configuration
|
||||
* @returns {Promise<any>} a promise with the data or error returned
|
||||
* @param url The URL to access
|
||||
* @param options Optional fetch configuration
|
||||
* @returns A promise with the data or error returned
|
||||
*/
|
||||
get(url: string, options = {}) {
|
||||
return fetch(urlFor(url), {
|
||||
credentials: credentialType(),
|
||||
get = <T extends any>(url: string, options: RequestInit = {}): Promise<T> => {
|
||||
// @ts-ignore
|
||||
return fetch(this.urlFor(url), {
|
||||
method: "GET",
|
||||
credentials: this.credentialType(),
|
||||
...options
|
||||
})
|
||||
.then(checkIfLoggedOut)
|
||||
.then(convertJSON)
|
||||
.catch(convertJSON)
|
||||
.then(checkForErrors);
|
||||
},
|
||||
}).then(this.handleResponse);
|
||||
};
|
||||
|
||||
/**
|
||||
* Perform a POST request
|
||||
*
|
||||
* @param url the URL to access
|
||||
* @param data the data to send
|
||||
* @returns {Promise<any>} a promise with the data or error returned
|
||||
* @param url The URL to access
|
||||
* @param data The data to send
|
||||
* @returns A promise with the data or error returned
|
||||
*/
|
||||
post(url: string, data: {}) {
|
||||
return fetch(urlFor(url), {
|
||||
post = <T extends any>(url: string, data: object): Promise<T> => {
|
||||
// @ts-ignore
|
||||
return fetch(this.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);
|
||||
},
|
||||
credentials: this.credentialType()
|
||||
}).then(this.handleResponse);
|
||||
};
|
||||
|
||||
/**
|
||||
* Perform a PUT request
|
||||
*
|
||||
* @param url the URL to access
|
||||
* @param data the data to send
|
||||
* @returns {Promise<any>} a promise with the data or error returned
|
||||
* @param url The URL to access
|
||||
* @param data The data to send
|
||||
* @returns A promise with the data or error returned
|
||||
*/
|
||||
put(url: string, data: {}) {
|
||||
return fetch(urlFor(url), {
|
||||
put = <T extends any>(url: string, data: object): Promise<T> => {
|
||||
// @ts-ignore
|
||||
return fetch(this.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);
|
||||
},
|
||||
credentials: this.credentialType()
|
||||
}).then(this.handleResponse);
|
||||
};
|
||||
|
||||
/**
|
||||
* Perform a DELETE request
|
||||
*
|
||||
* @param url the URL to access
|
||||
* @returns {Promise<any>} a promise with the data or error returned
|
||||
* @param url The URL to access
|
||||
* @returns A promise with the data or error returned
|
||||
*/
|
||||
delete(url: string) {
|
||||
return fetch(urlFor(url), {
|
||||
delete = <T extends any>(url: string): Promise<T> => {
|
||||
// @ts-ignore
|
||||
return fetch(this.urlFor(url), {
|
||||
method: "DELETE",
|
||||
credentials: credentialType()
|
||||
})
|
||||
.then(checkIfLoggedOut)
|
||||
.then(convertJSON)
|
||||
.catch(convertJSON)
|
||||
.then(checkForErrors);
|
||||
}
|
||||
};
|
||||
credentials: this.credentialType()
|
||||
}).then(this.handleResponse);
|
||||
};
|
||||
|
||||
/**
|
||||
* Get the URL for an endpoint
|
||||
*
|
||||
* @param endpoint The endpoint
|
||||
* @returns The URL for the endpoint
|
||||
*/
|
||||
urlFor = (endpoint: string): string => {
|
||||
return this.config.apiPath + "/" + endpoint;
|
||||
};
|
||||
|
||||
/**
|
||||
* Get the credential type for requests
|
||||
*
|
||||
* @returns The credential type
|
||||
*/
|
||||
credentialType = (): RequestCredentials => {
|
||||
// Development API requests may 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 this.config.developmentMode ? "include" : "same-origin";
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* 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
|
||||
* @param response The Response from fetch
|
||||
* @return If logged in, the response, otherwise a canceled promise
|
||||
*/
|
||||
const checkIfLoggedOut = (response: Response) => {
|
||||
export const checkIfLoggedOut = (response: Response): Promise<Response> => {
|
||||
if (api.loggedIn && response.status === 401) {
|
||||
// Clear the user's old session and refresh the page
|
||||
document.cookie =
|
||||
@@ -120,22 +146,24 @@ const checkIfLoggedOut = (response: Response) => {
|
||||
* @param data a Response or Error
|
||||
* @returns {*} a promise with the parsed JSON, or the error
|
||||
*/
|
||||
const convertJSON = (data: any): Promise<any> => {
|
||||
if (data.isCanceled || data instanceof Error) {
|
||||
export const convertJSON = <T extends any>(
|
||||
data: Response | Error | CanceledError
|
||||
): Promise<T> => {
|
||||
if ((data as CanceledError).isCanceled || data instanceof Error) {
|
||||
return Promise.reject(data);
|
||||
}
|
||||
|
||||
return data.json();
|
||||
return (data as Response).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
|
||||
* @returns A resolving promise with the data if no error, otherwise a
|
||||
* rejecting promise with the error
|
||||
*/
|
||||
const checkForErrors = (data: any): Promise<any> => {
|
||||
export const checkForErrors = <T extends any>(data: T): Promise<T> => {
|
||||
if (data.error) {
|
||||
return Promise.reject(data.error);
|
||||
}
|
||||
@@ -143,46 +171,20 @@ const checkForErrors = (data: any): Promise<any> => {
|
||||
return Promise.resolve(data);
|
||||
};
|
||||
|
||||
/**
|
||||
* Get the URL for an endpoint
|
||||
*
|
||||
* @param endpoint the endpoint
|
||||
* @returns {string} the URL for the endpoint
|
||||
*/
|
||||
const urlFor = (endpoint: string): string => {
|
||||
let apiLocation;
|
||||
|
||||
if (config.fakeAPI) {
|
||||
apiLocation = process.env.PUBLIC_URL + "/fakeAPI";
|
||||
} else {
|
||||
apiLocation = process.env.PUBLIC_URL + "/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";
|
||||
};
|
||||
|
||||
/**
|
||||
* Convert an object into GET parameters. The object must be flat (only
|
||||
* key-value pairs).
|
||||
*
|
||||
* @param params the parameters object
|
||||
* @returns {string} the parameters converted into GET parameter form
|
||||
* @param params The parameters object
|
||||
* @returns The parameters converted into GET parameter form
|
||||
*/
|
||||
export const paramsToString = (params: any) =>
|
||||
Object.keys(params)
|
||||
export const paramsToString = (params: {
|
||||
[key: string]: string | number;
|
||||
}): string => {
|
||||
return Object.keys(params)
|
||||
.map(key => key + "=" + params[key])
|
||||
.join("&");
|
||||
};
|
||||
|
||||
/**
|
||||
* Convert a time range into GET parameters
|
||||
@@ -190,8 +192,9 @@ export const paramsToString = (params: any) =>
|
||||
* @param range The time range to convert
|
||||
* @return The time range as GET parameters
|
||||
*/
|
||||
export const timeRangeToParams = (range: TimeRange) =>
|
||||
paramsToString({
|
||||
export const timeRangeToParams = (range: TimeRange) => {
|
||||
return paramsToString({
|
||||
from: range.from.unix(),
|
||||
until: range.until.unix()
|
||||
});
|
||||
};
|
||||
|
||||
+9
-3
@@ -15,8 +15,13 @@ import { reactI18nextModule } from "react-i18next";
|
||||
import config from "../config";
|
||||
import languages from "../languages.json";
|
||||
|
||||
export function setupI18n() {
|
||||
i18n
|
||||
/**
|
||||
* Set up the internationalization service
|
||||
*
|
||||
* @param ajax An optional ajax function to use when fetching translations
|
||||
*/
|
||||
export function setupI18n(ajax?: any) {
|
||||
return i18n
|
||||
.use(XHR)
|
||||
.use(LanguageDetector)
|
||||
.use(reactI18nextModule)
|
||||
@@ -46,7 +51,8 @@ export function setupI18n() {
|
||||
escapeValue: false
|
||||
},
|
||||
backend: {
|
||||
loadPath: process.env.PUBLIC_URL + "/i18n/{{lng}}/{{ns}}.json"
|
||||
loadPath: process.env.PUBLIC_URL + "/i18n/{{lng}}/{{ns}}.json",
|
||||
ajax
|
||||
},
|
||||
react: {
|
||||
// Wait until translations are loaded before rendering
|
||||
|
||||
@@ -21,9 +21,9 @@ const Blacklist: FunctionComponent<WithNamespaces> = props => {
|
||||
<ListPage
|
||||
title={`${t("Blacklist")} (${t("Exact")})`}
|
||||
placeholder={t("Add a domain or hostname (example.com or example)")}
|
||||
add={api.addBlacklist}
|
||||
remove={api.removeBlacklist}
|
||||
refresh={api.getBlacklist}
|
||||
onAdd={api.addBlacklist}
|
||||
onRemove={api.removeBlacklist}
|
||||
onRefresh={api.getBlacklist}
|
||||
isValid={isValidHostname}
|
||||
validationErrorMsg={t("Not a valid hostname")}
|
||||
{...props}
|
||||
|
||||
+2
-15
@@ -8,13 +8,7 @@
|
||||
* This file is copyright under the latest version of the EUPL.
|
||||
* Please see LICENSE file for your rights under this license. */
|
||||
|
||||
import React, {
|
||||
ChangeEvent,
|
||||
Component,
|
||||
FormEvent,
|
||||
Fragment,
|
||||
KeyboardEvent
|
||||
} from "react";
|
||||
import React, { ChangeEvent, Component, FormEvent, Fragment } from "react";
|
||||
import { Redirect } from "react-router-dom";
|
||||
import sha from "sha.js";
|
||||
import api from "../util/api";
|
||||
@@ -78,14 +72,7 @@ class Login extends Component<LoginProps, LoginState> {
|
||||
// Send the password to the API to authenticate the user
|
||||
api
|
||||
.authenticate(hashedPassword)
|
||||
.then(data => {
|
||||
// Verify status
|
||||
if (data.status !== "success") {
|
||||
console.log("Failed to log in:");
|
||||
console.log(data);
|
||||
return;
|
||||
}
|
||||
|
||||
.then(() => {
|
||||
api.loggedIn = true;
|
||||
|
||||
if (config.fakeAPI) {
|
||||
|
||||
@@ -21,9 +21,9 @@ const Regexlist: FunctionComponent<WithNamespaces> = props => {
|
||||
<ListPage
|
||||
title={`${t("Blacklist")} (${t("Regex")})`}
|
||||
placeholder={t("Input a regular expression")}
|
||||
add={api.addRegexlist}
|
||||
remove={api.removeRegexlist}
|
||||
refresh={api.getRegexlist}
|
||||
onAdd={api.addRegexlist}
|
||||
onRemove={api.removeRegexlist}
|
||||
onRefresh={api.getRegexlist}
|
||||
isValid={isValidRegex}
|
||||
validationErrorMsg={t("Not a valid regular expression")}
|
||||
{...props}
|
||||
|
||||
@@ -21,9 +21,9 @@ const Whitelist: FunctionComponent<WithNamespaces> = props => {
|
||||
<ListPage
|
||||
title={t("Whitelist")}
|
||||
placeholder={t("Add a domain or hostname (example.com or example)")}
|
||||
add={api.addWhitelist}
|
||||
remove={api.removeWhitelist}
|
||||
refresh={api.getWhitelist}
|
||||
onAdd={api.addWhitelist}
|
||||
onRemove={api.removeWhitelist}
|
||||
onRefresh={api.getWhitelist}
|
||||
isValid={isValidHostname}
|
||||
validationErrorMsg={t("Not a valid hostname")}
|
||||
{...props}
|
||||
|
||||
Reference in New Issue
Block a user