Merge pull request #216 from pi-hole/feature/top-blocked-clients

Top Blocked Clients
This commit is contained in:
Mark Drobnak
2019-05-28 15:57:37 -04:00
committed by GitHub
11 changed files with 279 additions and 40 deletions
+2 -1
View File
@@ -19,5 +19,6 @@
"No Domains Found": "No Domains Found",
"Queries Answered By Destination": "Queries Answered By Destination",
"Queries Over {{range}}": "Queries Over {{range}}",
"Clients Over {{range}}": "Clients Over {{range}}"
"Clients Over {{range}}": "Clients Over {{range}}",
"Top Blocked Clients": "Top Blocked Clients"
}
+26 -10
View File
@@ -156,10 +156,10 @@ function upstreams(length) {
};
}
function topList(length, max, fakeData) {
function topDomainList(length, max) {
const result = [];
const numbers = [];
const domains = unique(fakeData, length);
const domains = unique(faker.internet.domainName, length);
for (let i = 0; i < length; i++)
numbers.push(faker.random.number({ max: max }));
@@ -176,11 +176,11 @@ function topList(length, max, fakeData) {
return result;
}
function topBlocked(length) {
function topBlockedDomains(length) {
const totalQueries = faker.random.number();
return {
top_domains: topList(length, totalQueries, faker.internet.domainName),
top_domains: topDomainList(length, totalQueries),
blocked_queries: totalQueries
};
}
@@ -189,13 +189,12 @@ function topDomains(length) {
const totalQueries = faker.random.number();
return {
top_domains: topList(length, totalQueries, faker.internet.domainName),
top_domains: topDomainList(length, totalQueries),
total_queries: totalQueries
};
}
function topClients(length) {
const totalQueries = faker.random.number();
function topClientList(length, totalQueries) {
const top_clients = [];
const numbers = [];
const clients = unique(faker.internet.ip, length);
@@ -213,12 +212,27 @@ function topClients(length) {
});
}
return top_clients;
}
function topClients(length) {
const totalQueries = faker.random.number();
return {
top_clients: top_clients,
top_clients: topClientList(length, totalQueries),
total_queries: totalQueries
};
}
function topBlockedClients(length) {
const totalQueries = faker.random.number();
return {
top_clients: topClientList(length, totalQueries),
blocked_queries: totalQueries
};
}
function clientsOverTime(range, size) {
const startDate = pastDate();
const clients = unique(faker.internet.ip, size).map(ip => ({
@@ -402,17 +416,19 @@ write("public/fakeAPI/stats/summary", summary());
write("public/fakeAPI/stats/history", history(5000));
write("public/fakeAPI/stats/query_types", queryTypes());
write("public/fakeAPI/stats/upstreams", upstreams(3));
write("public/fakeAPI/stats/top_blocked", topBlocked(10));
write("public/fakeAPI/stats/top_blocked", topBlockedDomains(10));
write("public/fakeAPI/stats/top_domains", topDomains(10));
write("public/fakeAPI/stats/top_clients", topClients(10));
write("public/fakeAPI/stats/top_blocked_clients", topBlockedClients(10));
write("public/fakeAPI/stats/database/overTime/history", historyOverTime(144));
write("public/fakeAPI/stats/database/overTime/clients", clientsOverTime(144, 5));
write("public/fakeAPI/stats/database/summary", summary());
write("public/fakeAPI/stats/database/query_types", queryTypes());
write("public/fakeAPI/stats/database/upstreams", upstreams(3));
write("public/fakeAPI/stats/database/top_blocked", topBlocked(10));
write("public/fakeAPI/stats/database/top_blocked", topBlockedDomains(10));
write("public/fakeAPI/stats/database/top_domains", topDomains(10));
write("public/fakeAPI/stats/database/top_clients", topClients(10));
write("public/fakeAPI/stats/database/top_blocked_clients", topBlockedClients(10));
write("public/fakeAPI/auth", auth());
write("public/fakeAPI/version", getVersionInfo());
@@ -0,0 +1,109 @@
/* 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
* Top Blocked Clients component
*
* This file is copyright under the latest version of the EUPL.
* Please see LICENSE file for your rights under this license. */
import React, { ReactNode } from "react";
import { WithNamespaces, withNamespaces } from "react-i18next";
import api from "../../util/api";
import TopTable from "./TopTable";
import i18next from "i18next";
import { TimeRangeContext } from "../common/context/TimeRangeContext";
export interface TopBlockedClientsData {
blockedQueries: number;
topClients: Array<ApiClient>;
}
/**
* Transform the API data into the form used in generateRows
*
* @param data The API data
* @returns The parsed data
*/
export const transformData = (
data: ApiTopBlockedClients
): TopBlockedClientsData => ({
blockedQueries: data.blocked_queries,
topClients: data.top_clients
});
/**
* Generate rows of top blocked clients
*
* @param t The translation function
* @returns Rows of top blocked clients
*/
export const generateRows = (t: i18next.TranslationFunction) => (
data: TopBlockedClientsData
): ReactNode => {
return data.topClients.map(item => {
const percentage = (item.count / data.blockedQueries) * 100;
return (
<tr key={item.name + "|" + item.ip}>
<td>{item.name !== "" ? item.name : item.ip}</td>
<td>{item.count.toLocaleString()}</td>
<td style={{ verticalAlign: "middle" }}>
<div
className="progress"
title={t("{{percent}}% of {{total}}", {
percent: percentage.toFixed(1),
total: data.blockedQueries.toLocaleString()
})}
>
<div
className="progress-bar bg-danger"
style={{ width: percentage + "%" }}
/>
</div>
</td>
</tr>
);
});
};
const TopBlockedClients = ({
apiCall,
t,
...props
}: WithNamespaces & { apiCall: () => Promise<ApiTopBlockedClients> }) => (
<TopTable
{...props}
title={t("Top Blocked Clients")}
initialData={{
blockedQueries: 0,
topClients: []
}}
headers={[t("Client"), t("Requests"), t("Frequency")]}
emptyMessage={t("No Clients Found")}
isEmpty={data => data.topClients.length === 0}
apiCall={apiCall}
apiHandler={transformData}
generateRows={generateRows(t)}
/>
);
const TopBlockedClientsContainer = (props: WithNamespaces) => (
<TimeRangeContext.Consumer>
{context => (
<TopBlockedClients
{...props}
apiCall={() =>
context.range
? api.getTopBlockedClientsDb(context.range)
: api.getTopBlockedClients()
}
/>
)}
</TimeRangeContext.Consumer>
);
export default withNamespaces(["common", "dashboard"])(
TopBlockedClientsContainer
);
@@ -15,7 +15,7 @@ import TopTable from "./TopTable";
import i18next from "i18next";
import { TimeRangeContext } from "../common/context/TimeRangeContext";
export interface TopBlockedData {
export interface TopBlockedDomainsData {
totalBlocked: number;
topBlocked: Array<ApiTopDomainItem>;
}
@@ -26,7 +26,9 @@ export interface TopBlockedData {
* @param data the API data
* @returns {{totalBlocked: number, topBlocked: *}} the parsed data
*/
export const transformData = (data: ApiTopBlocked): TopBlockedData => ({
export const transformData = (
data: ApiTopBlockedDomains
): TopBlockedDomainsData => ({
totalBlocked: data.blocked_queries,
topBlocked: data.top_domains
});
@@ -38,7 +40,7 @@ export const transformData = (data: ApiTopBlocked): TopBlockedData => ({
* @returns {function(*): any[]} a function to generate rows of top blocked
*/
export const generateRows = (t: i18next.TranslationFunction) => (
data: TopBlockedData
data: TopBlockedDomainsData
) => {
return data.topBlocked.map(item => {
const percentage = (item.count / data.totalBlocked) * 100;
@@ -66,11 +68,11 @@ export const generateRows = (t: i18next.TranslationFunction) => (
});
};
const TopBlocked = ({
const TopBlockedDomains = ({
apiCall,
t,
...props
}: WithNamespaces & { apiCall: () => Promise<ApiTopBlocked> }) => (
}: WithNamespaces & { apiCall: () => Promise<ApiTopBlockedDomains> }) => (
<TopTable
{...props}
title={t("Top Blocked Domains")}
@@ -87,19 +89,21 @@ const TopBlocked = ({
/>
);
const TopBlockedContainer = (props: WithNamespaces) => (
const TopBlockedDomainsContainer = (props: WithNamespaces) => (
<TimeRangeContext.Consumer>
{context => (
<TopBlocked
<TopBlockedDomains
{...props}
apiCall={() =>
context.range
? api.getTopBlockedDb(context.range)
: api.getTopBlocked()
? api.getTopBlockedDomainsDb(context.range)
: api.getTopBlockedDomains()
}
/>
)}
</TimeRangeContext.Consumer>
);
export default withNamespaces(["common", "dashboard"])(TopBlockedContainer);
export default withNamespaces(["common", "dashboard"])(
TopBlockedDomainsContainer
);
@@ -0,0 +1,41 @@
/* 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
* TopBlockedClients component test
*
* This file is copyright under the latest version of the EUPL.
* Please see LICENSE file for your rights under this license. */
import React from "react";
import { generateRows, transformData } from "../TopBlockedClients";
const fakeData: ApiTopBlockedClients = {
top_clients: [
{ name: "", ip: "21.43.61.185", count: 37586 },
{ name: "", ip: "224.77.182.60", count: 28664 },
{ name: "", ip: "254.251.126.14", count: 28353 },
{ name: "", ip: "45.236.46.22", count: 26034 },
{ name: "", ip: "134.169.112.176", count: 23587 },
{ name: "", ip: "59.190.76.175", count: 14595 },
{ name: "", ip: "108.137.74.87", count: 11953 },
{ name: "", ip: "127.219.136.176", count: 11715 },
{ name: "", ip: "172.134.110.216", count: 5954 },
{ name: "", ip: "243.77.12.200", count: 4703 }
],
blocked_queries: 38451
};
it("transforms the API data correctly", async () => {
const data = transformData(fakeData);
expect(data.blockedQueries).toEqual(fakeData.blocked_queries);
expect(data.topClients).toEqual(fakeData.top_clients);
});
it("creates an appropriately sized table", async () => {
const rows = generateRows(key => key)(transformData(fakeData));
expect(rows).toHaveLength(fakeData.top_clients.length);
});
@@ -3,15 +3,15 @@
* Network-wide ad blocking via your own hardware.
*
* Web Interface
* TopBlocked component test
* TopBlockedDomains component test
*
* This file is copyright under the latest version of the EUPL.
* Please see LICENSE file for your rights under this license. */
import React from "react";
import { generateRows, transformData } from "../TopBlocked";
import { generateRows, transformData } from "../TopBlockedDomains";
const fakeData: ApiTopBlocked = {
const fakeData: ApiTopBlockedDomains = {
top_domains: [
{ domain: "jaron.info", count: 54316 },
{ domain: "candelario.info", count: 47470 },
+1
View File
@@ -154,6 +154,7 @@
.boxcontainer .app-header {
max-width: 1250px;
border-bottom-color: #3c8dbc;
}
.card-icon {
+6 -1
View File
@@ -177,7 +177,7 @@ interface ApiTopDomainItem {
count: number;
}
interface ApiTopBlocked {
interface ApiTopBlockedDomains {
top_domains: Array<ApiTopDomainItem>;
blocked_queries: number;
}
@@ -192,6 +192,11 @@ interface ApiTopClients {
total_queries: number;
}
interface ApiTopBlockedClients {
top_clients: Array<ApiClient>;
blocked_queries: number;
}
interface ApiClient {
name: string;
ip: string;
+34 -4
View File
@@ -104,7 +104,7 @@ describe("ApiClient", () => {
it("should call top blocked endpoint (top_domains?blocked=true)", async () => {
config.fakeAPI = false;
await expect(api.getTopBlocked()).resolves.toEqual(getData);
await expect(api.getTopBlockedDomains()).resolves.toEqual(getData);
expect(httpClient.get).toHaveBeenCalledWith(
"stats/top_domains?blocked=true"
);
@@ -112,7 +112,7 @@ describe("ApiClient", () => {
it("should call top blocked endpoint (top_blocked)", async () => {
config.fakeAPI = true;
await expect(api.getTopBlocked()).resolves.toEqual(getData);
await expect(api.getTopBlockedDomains()).resolves.toEqual(getData);
expect(httpClient.get).toHaveBeenCalledWith("stats/top_blocked");
});
@@ -121,6 +121,20 @@ describe("ApiClient", () => {
expect(httpClient.get).toHaveBeenCalledWith("stats/top_clients");
});
it("should call top blocked clients endpoint (top_clients?blocked=true)", async () => {
config.fakeAPI = false;
await expect(api.getTopBlockedClients()).resolves.toEqual(getData);
expect(httpClient.get).toHaveBeenCalledWith(
"stats/top_clients?blocked=true"
);
});
it("should call top blocked clients endpoint (top_blocked_clients)", async () => {
config.fakeAPI = true;
await expect(api.getTopBlockedClients()).resolves.toEqual(getData);
expect(httpClient.get).toHaveBeenCalledWith("stats/top_blocked_clients");
});
it("should call top history endpoint with params", async () => {
const params = { test: "params" };
await expect(api.getHistory(params)).resolves.toEqual(getData);
@@ -180,7 +194,7 @@ describe("ApiClient", () => {
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);
await expect(api.getTopBlockedDomainsDb(range)).resolves.toEqual(getData);
expect(httpClient.get).toHaveBeenCalledWith(
"stats/database/top_domains?blocked=true&" + rangeParams
);
@@ -188,7 +202,7 @@ describe("ApiClient", () => {
it("should call top blocked DB endpoint (top_blocked) with time range", async () => {
config.fakeAPI = true;
await expect(api.getTopBlockedDb(range)).resolves.toEqual(getData);
await expect(api.getTopBlockedDomainsDb(range)).resolves.toEqual(getData);
expect(httpClient.get).toHaveBeenCalledWith(
"stats/database/top_blocked?" + rangeParams
);
@@ -200,6 +214,22 @@ describe("ApiClient", () => {
"stats/database/top_clients?" + rangeParams
);
});
it("should call top blocked clients DB endpoint (top_clients?blocked=true) with time range", async () => {
config.fakeAPI = false;
await expect(api.getTopBlockedClientsDb(range)).resolves.toEqual(getData);
expect(httpClient.get).toHaveBeenCalledWith(
"stats/database/top_clients?blocked=true&" + rangeParams
);
});
it("should call top blocked clients DB endpoint (top_blocked_clients) with time range", async () => {
config.fakeAPI = true;
await expect(api.getTopBlockedClientsDb(range)).resolves.toEqual(getData);
expect(httpClient.get).toHaveBeenCalledWith(
"stats/database/top_blocked_clients?" + rangeParams
);
});
});
describe("dns calls", () => {
+30 -4
View File
@@ -97,7 +97,7 @@ export class ApiClient {
);
};
getTopBlocked(): Promise<ApiTopBlocked> {
getTopBlockedDomains = (): Promise<ApiTopBlockedDomains> => {
// 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 = this.http.config.fakeAPI
@@ -105,9 +105,11 @@ export class ApiClient {
: "stats/top_domains?blocked=true";
return this.http.get(url);
}
};
getTopBlockedDb(range: TimeRange): Promise<ApiTopBlocked> {
getTopBlockedDomainsDb = (
range: TimeRange
): Promise<ApiTopBlockedDomains> => {
// 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 = this.http.config.fakeAPI
@@ -115,7 +117,7 @@ export class ApiClient {
: "stats/database/top_domains?blocked=true&";
return this.http.get(url + timeRangeToParams(range));
}
};
getTopClients = (): Promise<ApiTopClients> => {
return this.http.get("stats/top_clients");
@@ -127,6 +129,30 @@ export class ApiClient {
);
};
getTopBlockedClients = (): Promise<ApiTopBlockedClients> => {
// The API uses a GET parameter to differentiate top clients from top
// blocked clients, but the fake API is not able to handle GET parameters
// right now.
const url = this.http.config.fakeAPI
? "stats/top_blocked_clients"
: "stats/top_clients?blocked=true";
return this.http.get(url);
};
getTopBlockedClientsDb = (
range: TimeRange
): Promise<ApiTopBlockedClients> => {
// The API uses a GET parameter to differentiate top clients from top
// blocked clients, but the fake API is not able to handle GET parameters
// right now.
const url = this.http.config.fakeAPI
? "stats/database/top_blocked_clients?"
: "stats/database/top_clients?blocked=true&";
return this.http.get(url + timeRangeToParams(range));
};
getHistory = (params: any): Promise<ApiHistoryResponse> => {
return this.http.get("stats/history?" + paramsToString(params));
};
+13 -7
View File
@@ -15,10 +15,11 @@ import ClientsGraph from "../components/dashboard/ClientsGraph";
import QueryTypesChart from "../components/dashboard/QueryTypesChart";
import UpstreamsChart from "../components/dashboard/UpstreamsChart";
import TopDomains from "../components/dashboard/TopDomains";
import TopBlocked from "../components/dashboard/TopBlocked";
import TopBlockedDomains from "../components/dashboard/TopBlockedDomains";
import TopClients from "../components/dashboard/TopClients";
import api from "../util/api";
import { TimeRangeSelectorContainer } from "../components/dashboard/TimeRangeSelector";
import TopBlockedClients from "../components/dashboard/TopBlockedClients";
export default () => (
<div className="animated fadeIn">
@@ -43,23 +44,28 @@ export default () => (
</div>
</div>
<div className="row">
<div className="col-md-6">
<div className="col-lg-6 col-md-12">
<QueryTypesChart />
</div>
<div className="col-md-6">
<div className="col-lg-6 col-md-12">
<UpstreamsChart />
</div>
</div>
<div className="row">
<div className="col-md-6 col-xl-4">
<div className="col-lg-6 col-md-12">
<TopDomains />
</div>
<div className="col-md-6 col-xl-4">
<TopBlocked />
<div className="col-lg-6 col-md-12">
<TopBlockedDomains />
</div>
<div className="col-md-6 col-xl-4">
</div>
<div className="row">
<div className="col-lg-6 col-md-12">
<TopClients />
</div>
<div className="col-lg-6 col-md-12">
<TopBlockedClients />
</div>
</div>
</Fragment>
) : null}