mirror of
https://github.com/gosticks/web.git
synced 2026-10-06 22:57:01 +00:00
Merge branch 'development' into feature/dhcp-rapid-commit
This commit is contained in:
@@ -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";
|
||||
|
||||
@@ -13,7 +13,11 @@ 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;
|
||||
|
||||
@@ -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,
|
||||
@@ -52,7 +56,7 @@ class DHCPInfo extends Component<WithNamespaces, DHCPInfoState> {
|
||||
};
|
||||
|
||||
private loadHandler: undefined | CancelablePromise<ApiDhcpSettings>;
|
||||
private updateHandler: undefined | CancelablePromise<ApiResultResponse>;
|
||||
private updateHandler: undefined | CancelablePromise<ApiSuccessResponse>;
|
||||
|
||||
loadDHCPInfo = () => {
|
||||
this.loadHandler = makeCancelable(api.getDHCPInfo());
|
||||
|
||||
@@ -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());
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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,284 @@
|
||||
/* 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 });
|
||||
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 });
|
||||
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 });
|
||||
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 });
|
||||
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
|
||||
|
||||
+1
-8
@@ -78,14 +78,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) {
|
||||
|
||||
Reference in New Issue
Block a user