diff --git a/package-lock.json b/package-lock.json
index 3186dc2..fab05fe 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -2529,15 +2529,15 @@
}
},
"@types/chart.js": {
- "version": "2.7.54",
- "resolved": "https://registry.npmjs.org/@types/chart.js/-/chart.js-2.7.54.tgz",
- "integrity": "sha512-BxIUR4mfk0zOqOPEu4gxLP5herra6INQLyFmgVE6JVRNNB+r36g2cd67nDUEEdD/EShZvaR33xausxOGv1+nbw==",
+ "version": "2.7.55",
+ "resolved": "https://registry.npmjs.org/@types/chart.js/-/chart.js-2.7.55.tgz",
+ "integrity": "sha512-CACGbxPkr0vcyrZFpDAr4dqpfgu/PiHWzsKAhXdY+g72Laebjn/dijr7ExwVv9JS/99KrhtKmfJ0vBEhPTJBTQ==",
"dev": true
},
"@types/cheerio": {
- "version": "0.22.11",
- "resolved": "https://registry.npmjs.org/@types/cheerio/-/cheerio-0.22.11.tgz",
- "integrity": "sha512-x0X3kPbholdJZng9wDMhb2swvUi3UYRNAuWAmIPIWlfgAJZp//cql/qblE7181Mg7SjWVwq6ldCPCLn5AY/e7w==",
+ "version": "0.22.12",
+ "resolved": "https://registry.npmjs.org/@types/cheerio/-/cheerio-0.22.12.tgz",
+ "integrity": "sha512-aczowyAJNfrkBV+HS8DyAA87OnvkqGrrOmm5s7V6Jbgimzv/1ZoAy91cLJX8GQrUS60KufD7EIzA2LbK8HV4hg==",
"dev": true,
"requires": {
"@types/node": "*"
@@ -2554,9 +2554,9 @@
}
},
"@types/enzyme": {
- "version": "3.9.4",
- "resolved": "https://registry.npmjs.org/@types/enzyme/-/enzyme-3.9.4.tgz",
- "integrity": "sha512-bQcwt5gcKnekrbci4hcapfE2J6rkkFbHM1l4VobLtSl4ogOfj0lvSxrdS6FftCakmJqqPBqdQCwb5KnlivL6SQ==",
+ "version": "3.10.1",
+ "resolved": "https://registry.npmjs.org/@types/enzyme/-/enzyme-3.10.1.tgz",
+ "integrity": "sha512-Jd2hvn3w/0rBH8qD2n+JLMq2GjmZoKTZc5TTg8q9cDnJ9jkR7gzPV+ZdipKwGkWpMRXKDm9kuc9UiUqsW5s20w==",
"dev": true,
"requires": {
"@types/cheerio": "*",
@@ -2658,9 +2658,9 @@
}
},
"@types/node": {
- "version": "12.0.10",
- "resolved": "https://registry.npmjs.org/@types/node/-/node-12.0.10.tgz",
- "integrity": "sha512-LcsGbPomWsad6wmMNv7nBLw7YYYyfdYcz6xryKYQhx89c3XXan+8Q6AJ43G5XDIaklaVkK3mE4fCb0SBvMiPSQ==",
+ "version": "12.0.12",
+ "resolved": "https://registry.npmjs.org/@types/node/-/node-12.0.12.tgz",
+ "integrity": "sha512-Uy0PN4R5vgBUXFoJrKryf5aTk3kJ8Rv3PdlHjl6UaX+Cqp1QE0yPQ68MPXGrZOfG7gZVNDIJZYyot0B9ubXUrQ==",
"dev": true
},
"@types/prop-types": {
@@ -2676,9 +2676,9 @@
"dev": true
},
"@types/react": {
- "version": "16.8.22",
- "resolved": "https://registry.npmjs.org/@types/react/-/react-16.8.22.tgz",
- "integrity": "sha512-C3O1yVqk4sUXqWyx0wlys76eQfhrQhiDhDlHBrjER76lR2S2Agiid/KpOU9oCqj1dISStscz7xXz1Cg8+sCQeA==",
+ "version": "16.8.23",
+ "resolved": "https://registry.npmjs.org/@types/react/-/react-16.8.23.tgz",
+ "integrity": "sha512-abkEOIeljniUN9qB5onp++g0EY38h7atnDHxwKUFz1r3VH1+yG1OKi2sNPTyObL40goBmfKFpdii2lEzwLX1cA==",
"dev": true,
"requires": {
"@types/prop-types": "*",
@@ -8955,14 +8955,22 @@
}
},
"fs-extra": {
- "version": "8.0.1",
- "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-8.0.1.tgz",
- "integrity": "sha512-W+XLrggcDzlle47X/XnS7FXrXu9sDo+Ze9zpndeBxdgv88FHLm1HtmkhEwavruS6koanBjp098rUpHs65EmG7A==",
+ "version": "8.1.0",
+ "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-8.1.0.tgz",
+ "integrity": "sha512-yhlQgA6mnOJUKOsRUFsgJdQCvkKhcz8tlZG5HBQfReYZy46OwLcY+Zia0mtdHsOo9y/hP+CxMN0TU9QxoOtG4g==",
"dev": true,
"requires": {
- "graceful-fs": "^4.1.2",
+ "graceful-fs": "^4.2.0",
"jsonfile": "^4.0.0",
"universalify": "^0.1.0"
+ },
+ "dependencies": {
+ "graceful-fs": {
+ "version": "4.2.0",
+ "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.0.tgz",
+ "integrity": "sha512-jpSvDPV4Cq/bgtpndIWbI5hmYxhQGHPC4d4cqBPb4DLniCfhJokdXhwhaDuLBGLQdvvRum/UiX6ECVIPvDXqdg==",
+ "dev": true
+ }
}
},
"fs-minipass": {
@@ -10519,17 +10527,17 @@
"dev": true
},
"i18next": {
- "version": "17.0.4",
- "resolved": "https://registry.npmjs.org/i18next/-/i18next-17.0.4.tgz",
- "integrity": "sha512-+lwmv3FT8Sv/HwVPjkR6rtEFhgOqt9L/CTehzyxvL/NdkeUYbFZJfE57MsBToB6LFWg3d0sZJIVgYqCpWzUyLQ==",
+ "version": "17.0.6",
+ "resolved": "https://registry.npmjs.org/i18next/-/i18next-17.0.6.tgz",
+ "integrity": "sha512-bdNhzhcM6RG5m82RypVguCrAQNie/ycxW0Q5C6K9UDWD5hqApZfdJFbj4Ikz9jxIR+Ja1eg0yCQLhlCT+opwIg==",
"requires": {
"@babel/runtime": "^7.3.1"
},
"dependencies": {
"@babel/runtime": {
- "version": "7.4.5",
- "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.4.5.tgz",
- "integrity": "sha512-TuI4qpWZP6lGOGIuGWtp9sPluqYICmbk8T/1vpSysqJxRPkudh/ofFWyqdcMsDf2s7KvDL4/YHgKyvcS3g9CJQ==",
+ "version": "7.5.1",
+ "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.5.1.tgz",
+ "integrity": "sha512-g+hmPKs16iewFSmW57NkH9xpPkuYD1RV3UE2BCkXx9j+nhhRb9hsiSxPmEa67j35IecTQdn4iyMtHMbt5VoREg==",
"requires": {
"regenerator-runtime": "^0.13.2"
}
@@ -10850,9 +10858,9 @@
"dev": true
},
"ionicons": {
- "version": "4.5.8",
- "resolved": "https://registry.npmjs.org/ionicons/-/ionicons-4.5.8.tgz",
- "integrity": "sha512-Zib/fFGHt+r9tWrvZnrxVj22cO4NVj+ZOb76v5yiOyvb0LSTbbReECDg7lEU8e+YR69qCtWHruStJ+b4G/OWxg=="
+ "version": "4.6.1",
+ "resolved": "https://registry.npmjs.org/ionicons/-/ionicons-4.6.1.tgz",
+ "integrity": "sha512-HIT6nAozXnM5yO9an0QpqABM4QFs1xtOjTz4Neyqbs0+fbPqN3p2PwJuh99WR9YaYGrFPzSCW6/n+LLL6C+xeg=="
},
"ip": {
"version": "1.1.5",
@@ -18665,18 +18673,18 @@
"dev": true
},
"react-i18next": {
- "version": "10.11.2",
- "resolved": "https://registry.npmjs.org/react-i18next/-/react-i18next-10.11.2.tgz",
- "integrity": "sha512-Ru+WonG9BLILDg/By7Pw0Lne2I7HVLRyhyvDtar22tNNu6de2D89zAKl2RzK2jgq7m7l+BFSnLpz4kX7ZPV+JQ==",
+ "version": "10.11.4",
+ "resolved": "https://registry.npmjs.org/react-i18next/-/react-i18next-10.11.4.tgz",
+ "integrity": "sha512-/CWXaf3a5BLNeVnBGxzWOIZLQgSNEc2LWHX4ZaJb7ww0xgY0S5K9HRAMzJIHeHGe7jfpSraprD66VDblWb4ZXA==",
"requires": {
"@babel/runtime": "^7.3.1",
"html-parse-stringify2": "2.0.1"
},
"dependencies": {
"@babel/runtime": {
- "version": "7.4.5",
- "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.4.5.tgz",
- "integrity": "sha512-TuI4qpWZP6lGOGIuGWtp9sPluqYICmbk8T/1vpSysqJxRPkudh/ofFWyqdcMsDf2s7KvDL4/YHgKyvcS3g9CJQ==",
+ "version": "7.5.1",
+ "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.5.1.tgz",
+ "integrity": "sha512-g+hmPKs16iewFSmW57NkH9xpPkuYD1RV3UE2BCkXx9j+nhhRb9hsiSxPmEa67j35IecTQdn4iyMtHMbt5VoREg==",
"requires": {
"regenerator-runtime": "^0.13.2"
}
diff --git a/package.json b/package.json
index 3d4ec4a..35ff693 100644
--- a/package.json
+++ b/package.json
@@ -4,15 +4,15 @@
"private": true,
"homepage": ".",
"devDependencies": {
- "@types/chart.js": "^2.7.54",
- "@types/enzyme": "^3.9.4",
+ "@types/chart.js": "^2.7.55",
+ "@types/enzyme": "^3.10.1",
"@types/enzyme-adapter-react-16": "^1.0.5",
"@types/fetch-mock": "^7.3.1",
"@types/jest": "^24.0.15",
"@types/lodash.debounce": "^4.0.6",
"@types/lodash.isequal": "^4.5.5",
- "@types/node": "^12.0.10",
- "@types/react": "^16.8.22",
+ "@types/node": "^12.0.12",
+ "@types/react": "^16.8.23",
"@types/react-bootstrap-daterangepicker": "0.0.26",
"@types/react-bootstrap-typeahead": "^3.4.5",
"@types/react-dom": "^16.8.4",
@@ -24,7 +24,7 @@
"enzyme-adapter-react-16": "^1.14.0",
"faker": "^4.1.0",
"fetch-mock": "^7.3.3",
- "fs-extra": "^8.0.1",
+ "fs-extra": "^8.1.0",
"jest-enzyme": "^7.0.2",
"jest-localstorage-mock": "^2.4.0",
"multi-progress": "^2.0.0",
@@ -44,10 +44,10 @@
"bootstrap": "^4.3.1",
"bootstrap-daterangepicker": "^3.0.5",
"chart.js": "^2.8.0",
- "i18next": "^17.0.4",
+ "i18next": "^17.0.6",
"i18next-browser-languagedetector": "^3.0.1",
"i18next-xhr-backend": "^3.0.0",
- "ionicons": "^4.5.8",
+ "ionicons": "^4.6.1",
"lodash.debounce": "^4.0.8",
"lodash.isequal": "^4.5.0",
"moment": "^2.24.0",
@@ -57,7 +57,7 @@
"react-bootstrap-typeahead": "^3.4.5",
"react-chartjs-2": "^2.7.6",
"react-dom": "^16.8.6",
- "react-i18next": "^10.11.2",
+ "react-i18next": "^10.11.4",
"react-router-dom": "^5.0.1",
"react-table": "^6.10.0",
"reactstrap": "^8.0.0",
diff --git a/public/i18n/en/settings.json b/public/i18n/en/settings.json
index 59bafce..aef67ae 100644
--- a/public/i18n/en/settings.json
+++ b/public/i18n/en/settings.json
@@ -27,5 +27,6 @@
"Successfully saved settings": "Successfully saved settings",
"Detected custom upstream server": "Detected custom upstream server",
"DNS Options": "DNS Options",
- "Rapid Commit": "Rapid Commit"
+ "Rapid Commit": "Rapid Commit",
+ "Prefix length (CIDR)": "Prefix length (CIDR)"
}
\ No newline at end of file
diff --git a/scripts/make-fake-data.js b/scripts/make-fake-data.js
index c18a0a5..c492d53 100644
--- a/scripts/make-fake-data.js
+++ b/scripts/make-fake-data.js
@@ -377,11 +377,12 @@ function getDNSInfo() {
},
conditional_forwarding: {
enabled: faker.random.boolean(),
- router_ip: faker.internet.ip(),
+ ip: faker.internet.ip(),
domain: faker.random
.word()
.toLowerCase()
- .split(" ", 2)[0]
+ .split(" ", 2)[0],
+ cidr: faker.random.arrayElement([8, 16, 24, 32])
}
};
}
diff --git a/src/components/common/__tests__/Footer.test.tsx b/src/components/common/__tests__/Footer.test.tsx
index 1df44f0..ac69aef 100644
--- a/src/components/common/__tests__/Footer.test.tsx
+++ b/src/components/common/__tests__/Footer.test.tsx
@@ -13,5 +13,5 @@ import { shallow } from "enzyme";
import Footer from "../Footer";
it("renders without crashing", () => {
- shallow();
+ shallow().dive();
});
diff --git a/src/components/dashboard/ClientsGraph.tsx b/src/components/dashboard/ClientsGraph.tsx
index 2bf1888..c6ac9da 100644
--- a/src/components/dashboard/ClientsGraph.tsx
+++ b/src/components/dashboard/ClientsGraph.tsx
@@ -14,7 +14,7 @@ import { Line } from "react-chartjs-2";
import { WithTranslation, withTranslation } from "react-i18next";
import moment from "moment";
import { getIntervalForRange } from "../../util/graphUtils";
-import api from "../../util/api";
+import api, { ApiClient } from "../../util/api";
import ChartTooltip from "./ChartTooltip";
import { WithAPIData } from "../common/WithAPIData";
import { ChartDataSets, ChartOptions, TimeUnit } from "chart.js";
@@ -242,17 +242,23 @@ export const TranslatedClientsGraph = withTranslation([
"time-ranges"
])(ClientsGraph);
-export const ClientsGraphContainer = () => (
+export interface ClientsGraphContainerProps {
+ apiClient: ApiClient;
+}
+
+export const ClientsGraphContainer = ({
+ apiClient
+}: ClientsGraphContainerProps) => (
{context => (
context.range
- ? api.getClientsGraphDb(
+ ? apiClient.getClientsGraphDb(
context.range,
getIntervalForRange(context.range)
)
- : api.getClientsGraph()
+ : apiClient.getClientsGraph()
}
repeatOptions={
context.range
@@ -271,3 +277,7 @@ export const ClientsGraphContainer = () => (
)}
);
+
+ClientsGraphContainer.defaultProps = {
+ apiClient: api
+};
diff --git a/src/components/dashboard/TopBlockedClients.tsx b/src/components/dashboard/TopBlockedClients.tsx
index 0413c6f..c547014 100644
--- a/src/components/dashboard/TopBlockedClients.tsx
+++ b/src/components/dashboard/TopBlockedClients.tsx
@@ -17,7 +17,7 @@ import { TimeRangeContext } from "../common/context/TimeRangeContext";
export interface TopBlockedClientsData {
blockedQueries: number;
- topClients: Array;
+ topClients: Array;
}
/**
diff --git a/src/components/dashboard/TopClients.tsx b/src/components/dashboard/TopClients.tsx
index 7c4998a..1a90c9d 100644
--- a/src/components/dashboard/TopClients.tsx
+++ b/src/components/dashboard/TopClients.tsx
@@ -17,7 +17,7 @@ import { TimeRangeContext } from "../common/context/TimeRangeContext";
export interface TopClientsData {
totalQueries: number;
- topClients: Array;
+ topClients: Array;
}
/**
diff --git a/src/components/dashboard/__tests__/ClientsGraph.test.tsx b/src/components/dashboard/__tests__/ClientsGraph.test.tsx
index e717795..281e3b0 100644
--- a/src/components/dashboard/__tests__/ClientsGraph.test.tsx
+++ b/src/components/dashboard/__tests__/ClientsGraph.test.tsx
@@ -11,10 +11,21 @@
import React from "react";
import { shallow } from "enzyme";
import {
+ ClientsGraph,
+ ClientsGraphContainer,
loadingProps,
transformData,
TranslatedClientsGraph
} from "../ClientsGraph";
+import {
+ TimeRange,
+ TimeRangeContext,
+ TimeRangeContextType
+} from "../../common/context/TimeRangeContext";
+import moment from "moment";
+import { Line } from "react-chartjs-2";
+import { ChartData, ChartTooltipItem } from "chart.js";
+import { ApiClient } from "../../../util/api";
const fakeData: ApiClientsGraph = {
over_time: [
@@ -38,6 +49,8 @@ const fakeData: ApiClientsGraph = {
]
};
+const tick = global.tick;
+
it("shows loading indicator correctly", () => {
const wrapper = shallow().dive();
@@ -62,3 +75,188 @@ it("loads API data correctly", async () => {
expect(props.datasets[1].label).toEqual(fakeData.clients[1].ip);
expect(props.datasets[0].data!.length).toEqual(fakeData.over_time.length - 1);
});
+
+it("should use all of the data if there is a time range set", () => {
+ const range: TimeRange = {
+ name: "test",
+ from: moment().subtract(1, "hour"),
+ until: moment()
+ };
+ const props = transformData(fakeData, range);
+
+ expect(props.datasets[0].data!.length).toEqual(fakeData.over_time.length);
+});
+
+it("should use hour as the time unit if the time range is less than a day", () => {
+ const range: TimeRange = {
+ name: "test",
+ from: moment().subtract(1, "hour"),
+ until: moment()
+ };
+ const props = transformData(fakeData, range);
+
+ expect(props.timeUnit).toEqual("hour");
+});
+
+it("should use day as the time unit if the time range is more than a day", () => {
+ const range: TimeRange = {
+ name: "test",
+ from: moment().subtract(2, "days"),
+ until: moment()
+ };
+ const props = transformData(fakeData, range);
+
+ expect(props.timeUnit).toEqual("day");
+});
+
+it("should show the date range in the tooltip title", () => {
+ const t = jest.fn(key => key);
+ const wrapper = shallow(
+ // @ts-ignore Ignore the missing i18n props
+
+ );
+
+ const titleFunc = wrapper.find(Line).props().options!.tooltips!.callbacks!
+ .title!;
+ const tooltipItem: ChartTooltipItem = {
+ datasetIndex: 0,
+ index: 0,
+ xLabel: "04:02",
+ yLabel: ""
+ };
+
+ const result = titleFunc([tooltipItem], {});
+
+ expect(result).toEqual("Client activity from {{from}} to {{to}}");
+ expect(t).toHaveBeenCalledWith("Client activity from {{from}} to {{to}}", {
+ from: "03:57:00",
+ to: "04:06:59"
+ });
+});
+
+it("should show the client and count in the tooltip label", () => {
+ const wrapper = shallow(
+
+ ).dive();
+
+ const labelFunc = wrapper.find(Line).props().options!.tooltips!.callbacks!
+ .label!;
+ const tooltipItem: ChartTooltipItem = {
+ datasetIndex: 0,
+ index: 0,
+ xLabel: "xLabel",
+ yLabel: "yLabel"
+ };
+ const data: ChartData = {
+ datasets: [
+ {
+ label: "datasetLabel"
+ }
+ ]
+ };
+
+ expect(labelFunc(tooltipItem, data)).toEqual("datasetLabel: yLabel");
+});
+
+it("should use the normal API call when there is no time range", () => {
+ const context: TimeRangeContextType = {
+ range: null,
+ update: () => {}
+ };
+ const apiClient = ({
+ getClientsGraph: jest.fn(() => Promise.reject({ isCanceled: true })),
+ getClientsGraphDb: jest.fn(() => Promise.reject({ isCanceled: true }))
+ } as any) as ApiClient;
+
+ shallow(
+
+
+
+ )
+ .dive()
+ .dive()
+ .dive();
+
+ expect(apiClient.getClientsGraph).toHaveBeenCalled();
+ expect(apiClient.getClientsGraphDb).not.toHaveBeenCalled();
+});
+
+it("should use the DB API call when there is a time range", () => {
+ const context: TimeRangeContextType = {
+ range: {
+ name: "test",
+ from: moment().subtract(1, "day"),
+ until: moment()
+ },
+ update: () => {}
+ };
+ const apiClient = ({
+ getClientsGraph: jest.fn(() => Promise.reject({ isCanceled: true })),
+ getClientsGraphDb: jest.fn(() => Promise.reject({ isCanceled: true }))
+ } as any) as ApiClient;
+
+ shallow(
+
+
+
+ )
+ .dive()
+ .dive()
+ .dive();
+
+ expect(apiClient.getClientsGraph).not.toHaveBeenCalled();
+ expect(apiClient.getClientsGraphDb).toHaveBeenCalledWith(context.range, 600);
+});
+
+it("should transform the data and render if the API returns data", async () => {
+ const context: TimeRangeContextType = {
+ range: null,
+ update: () => {}
+ };
+ const apiClient = ({
+ getClientsGraph: () => Promise.resolve(fakeData)
+ } as any) as ApiClient;
+
+ const wrapper = shallow(
+
+
+
+ )
+ .dive()
+ .dive()
+ .dive();
+
+ // Let the API call resolve
+ await tick();
+ wrapper.update();
+
+ const actualProps = wrapper.find(TranslatedClientsGraph).props();
+ const expectedProps = transformData(fakeData, null);
+ expect(actualProps).toEqual(expectedProps);
+});
+
+it("should render as loading if the API fails to return data", async () => {
+ const context: TimeRangeContextType = {
+ range: null,
+ update: () => {}
+ };
+ const apiClient = ({
+ getClientsGraph: () => Promise.reject({ error: {} })
+ } as any) as ApiClient;
+
+ const wrapper = shallow(
+
+
+
+ )
+ .dive()
+ .dive()
+ .dive();
+
+ // Let the API call resolve
+ await tick();
+ wrapper.update();
+
+ const actualProps = wrapper.find(TranslatedClientsGraph).props();
+ expect(actualProps).toEqual(loadingProps);
+});
diff --git a/src/components/settings/ConditionalForwardingSettings.tsx b/src/components/settings/ConditionalForwardingSettings.tsx
index df00d47..23547c3 100644
--- a/src/components/settings/ConditionalForwardingSettings.tsx
+++ b/src/components/settings/ConditionalForwardingSettings.tsx
@@ -14,14 +14,16 @@ import i18next from "i18next";
export interface ConditionalForwardingObject {
enabled: boolean;
- routerIp: string;
+ ip: string;
domain: string;
+ cidr: number;
}
export interface ConditionalForwardingSettingsProps {
settings: ConditionalForwardingObject;
onUpdate: (settings: ConditionalForwardingObject) => void;
isRouterIpValid: boolean;
+ isCidrValid: boolean;
isDomainValid: boolean;
t: i18next.TFunction;
}
@@ -30,6 +32,7 @@ const ConditionalForwardingSettings = ({
settings,
onUpdate,
isRouterIpValid,
+ isCidrValid,
isDomainValid,
t
}: ConditionalForwardingSettingsProps) => (
@@ -52,12 +55,39 @@ const ConditionalForwardingSettings = ({
onUpdate({ ...settings, routerIp: e.target.value })}
+ value={settings.ip}
+ onChange={e => onUpdate({ ...settings, ip: e.target.value })}
invalid={!isRouterIpValid}
/>
+
+
+
+ {
+ let cidr = parseInt(e.target.value);
+
+ if (e.target.value.length === 0) {
+ // Use -1 as an internal representation of the empty string.
+ // It will show the form as invalid and make the text field
+ // function as the user expects it to.
+ cidr = -1;
+ } else if (isNaN(cidr)) {
+ return;
+ }
+
+ onUpdate({ ...settings, cidr });
+ }}
+ />
+
+
diff --git a/src/types/api.d.ts b/src/types/api.d.ts
index bd1f2cb..9df7307 100644
--- a/src/types/api.d.ts
+++ b/src/types/api.d.ts
@@ -54,8 +54,9 @@ interface ApiDnsSettings {
};
conditional_forwarding: {
enabled: boolean;
- router_ip: string;
+ ip: string;
domain: string;
+ cidr: number;
};
}
@@ -188,7 +189,7 @@ interface ApiTopDomains {
}
interface ApiTopClients {
- top_clients: Array;
+ top_clients: Array;
total_queries: number;
}
@@ -197,7 +198,7 @@ interface ApiTopBlockedClients {
blocked_queries: number;
}
-interface ApiClient {
+interface ApiClientData {
name: string;
ip: string;
count: number;
diff --git a/src/util/__tests__/validate.test.tsx b/src/util/__tests__/validate.test.tsx
index 58f5bf0..1677ef9 100644
--- a/src/util/__tests__/validate.test.tsx
+++ b/src/util/__tests__/validate.test.tsx
@@ -12,7 +12,10 @@ import {
isPositiveNumber,
isValidDomain,
isValidIpv4,
+ isValidIpv4Cidr,
isValidIpv4OptionalPort,
+ isValidIpv6,
+ isValidIpv6Cidr,
isValidIpv6OptionalPort,
isValidRegex
} from "../validate";
@@ -229,6 +232,70 @@ describe("Testing the validation functions", () => {
});
});
+ describe("isValidIpv4Cidr", () => {
+ it("passes 8", () => {
+ expect(isValidIpv4Cidr("8")).toBe(true);
+ });
+
+ it("passes 16", () => {
+ expect(isValidIpv4Cidr("16")).toBe(true);
+ });
+
+ it("passes 24", () => {
+ expect(isValidIpv4Cidr("24")).toBe(true);
+ });
+
+ it("passes 32", () => {
+ expect(isValidIpv4Cidr("32")).toBe(true);
+ });
+
+ it("fails -1", () => {
+ expect(isValidIpv4Cidr("-1")).toBe(false);
+ });
+
+ it("fails 0", () => {
+ expect(isValidIpv4Cidr("0")).toBe(false);
+ });
+
+ it("fails 28", () => {
+ expect(isValidIpv4Cidr("28")).toBe(false);
+ });
+
+ it("fails 40", () => {
+ expect(isValidIpv4Cidr("40")).toBe(false);
+ });
+ });
+
+ describe("isValidIpv6", () => {
+ it("passes 1fff:0:a88:85a3::ac1f", () => {
+ expect(isValidIpv6("1fff:0:a88:85a3::ac1f")).toBe(true);
+ });
+
+ it("passes ::1", () => {
+ expect(isValidIpv6("::1")).toBe(true);
+ });
+
+ it("fails [::1]:5353", () => {
+ expect(isValidIpv6("[::1]:5353")).toBe(false);
+ });
+
+ it("fails [1fff:0:a88:85a3::ac1f]:8001", () => {
+ expect(isValidIpv6("[1fff:0:a88:85a3::ac1f]:8001")).toBe(false);
+ });
+
+ it("fails 192.168.1.1", () => {
+ expect(isValidIpv6("192.168.1.1")).toBe(false);
+ });
+
+ it("fails 192.168.1.1:53", () => {
+ expect(isValidIpv6("192.168.1.1:53")).toBe(false);
+ });
+
+ it("fails [::1]", () => {
+ expect(isValidIpv6("[::1]")).toBe(false);
+ });
+ });
+
describe("isValidIpv6OptionalPort", () => {
it("passes 1fff:0:a88:85a3::ac1f", () => {
expect(isValidIpv6OptionalPort("1fff:0:a88:85a3::ac1f")).toBe(true);
@@ -260,4 +327,38 @@ describe("Testing the validation functions", () => {
expect(isValidIpv6OptionalPort("[::1]")).toBe(false);
});
});
+
+ describe("isValidIpv6Cidr", () => {
+ it("passes 8", () => {
+ expect(isValidIpv6Cidr("8")).toBe(true);
+ });
+
+ it("passes 64", () => {
+ expect(isValidIpv6Cidr("64")).toBe(true);
+ });
+
+ it("passes 56", () => {
+ expect(isValidIpv6Cidr("56")).toBe(true);
+ });
+
+ it("passes 128", () => {
+ expect(isValidIpv6Cidr("128")).toBe(true);
+ });
+
+ it("fails -1", () => {
+ expect(isValidIpv6Cidr("-1")).toBe(false);
+ });
+
+ it("fails 0", () => {
+ expect(isValidIpv6Cidr("0")).toBe(false);
+ });
+
+ it("fails 63", () => {
+ expect(isValidIpv6Cidr("63")).toBe(false);
+ });
+
+ it("fails 150", () => {
+ expect(isValidIpv6Cidr("150")).toBe(false);
+ });
+ });
});
diff --git a/src/util/validate.tsx b/src/util/validate.tsx
index b428ece..712258d 100644
--- a/src/util/validate.tsx
+++ b/src/util/validate.tsx
@@ -95,6 +95,25 @@ export function isValidIpv4OptionalPort(address: string): boolean {
);
}
+/**
+ * Check if the string is a valid IPv4 CIDR number which dnsmasq accepts
+ *
+ * @param cidr The string to check
+ */
+export function isValidIpv4Cidr(cidr: string): boolean {
+ return ["8", "16", "24", "32"].includes(cidr);
+}
+
+/**
+ * Check if the string is a valid IPv6 address. This function does not do a
+ * thorough check of the IPv6 address; the backend will check it in detail.
+ *
+ * @param address
+ */
+export function isValidIpv6(address: string): boolean {
+ return /^[a-fA-F0-9:]+$/.test(address);
+}
+
/**
* Check if the string is a valid IPv6 address, and it can contain an optional
* port. This function does not do a thorough check of the IPv6 address; the
@@ -108,3 +127,14 @@ export function isValidIpv4OptionalPort(address: string): boolean {
export function isValidIpv6OptionalPort(address: string): boolean {
return /^(\[[a-fA-F0-9:]+]:\d+|[a-fA-F0-9:]+)$/.test(address);
}
+
+/**
+ * Check if the string is a valid IPv6 CIDR number which dnsmasq accepts
+ *
+ * @param cidr The string to check
+ */
+export function isValidIpv6Cidr(cidr: string): boolean {
+ const cidrNum = parseInt(cidr);
+
+ return !isNaN(cidrNum) && cidrNum > 0 && cidrNum <= 128 && cidrNum % 4 === 0;
+}