mirror of
https://github.com/gosticks/web.git
synced 2026-08-17 06:20:19 +00:00
Merge branch 'development' into feature/auto-login-if-no-password
This commit is contained in:
Generated
+42
-34
@@ -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"
|
||||
}
|
||||
|
||||
+8
-8
@@ -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",
|
||||
|
||||
@@ -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)"
|
||||
}
|
||||
@@ -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])
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
@@ -13,5 +13,5 @@ import { shallow } from "enzyme";
|
||||
import Footer from "../Footer";
|
||||
|
||||
it("renders without crashing", () => {
|
||||
shallow(<Footer />);
|
||||
shallow(<Footer />).dive();
|
||||
});
|
||||
|
||||
@@ -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) => (
|
||||
<TimeRangeContext.Consumer>
|
||||
{context => (
|
||||
<WithAPIData
|
||||
apiCall={() =>
|
||||
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 = () => (
|
||||
)}
|
||||
</TimeRangeContext.Consumer>
|
||||
);
|
||||
|
||||
ClientsGraphContainer.defaultProps = {
|
||||
apiClient: api
|
||||
};
|
||||
|
||||
@@ -17,7 +17,7 @@ import { TimeRangeContext } from "../common/context/TimeRangeContext";
|
||||
|
||||
export interface TopBlockedClientsData {
|
||||
blockedQueries: number;
|
||||
topClients: Array<ApiClient>;
|
||||
topClients: Array<ApiClientData>;
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -17,7 +17,7 @@ import { TimeRangeContext } from "../common/context/TimeRangeContext";
|
||||
|
||||
export interface TopClientsData {
|
||||
totalQueries: number;
|
||||
topClients: Array<ApiClient>;
|
||||
topClients: Array<ApiClientData>;
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -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(<TranslatedClientsGraph {...loadingProps} />).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
|
||||
<ClientsGraph {...transformData(fakeData, null)} t={t} />
|
||||
);
|
||||
|
||||
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(
|
||||
<TranslatedClientsGraph {...transformData(fakeData, null)} />
|
||||
).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(
|
||||
<TimeRangeContext.Provider value={context}>
|
||||
<ClientsGraphContainer apiClient={apiClient} />
|
||||
</TimeRangeContext.Provider>
|
||||
)
|
||||
.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(
|
||||
<TimeRangeContext.Provider value={context}>
|
||||
<ClientsGraphContainer apiClient={apiClient} />
|
||||
</TimeRangeContext.Provider>
|
||||
)
|
||||
.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(
|
||||
<TimeRangeContext.Provider value={context}>
|
||||
<ClientsGraphContainer apiClient={apiClient} />
|
||||
</TimeRangeContext.Provider>
|
||||
)
|
||||
.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(
|
||||
<TimeRangeContext.Provider value={context}>
|
||||
<ClientsGraphContainer apiClient={apiClient} />
|
||||
</TimeRangeContext.Provider>
|
||||
)
|
||||
.dive()
|
||||
.dive()
|
||||
.dive();
|
||||
|
||||
// Let the API call resolve
|
||||
await tick();
|
||||
wrapper.update();
|
||||
|
||||
const actualProps = wrapper.find(TranslatedClientsGraph).props();
|
||||
expect(actualProps).toEqual(loadingProps);
|
||||
});
|
||||
|
||||
@@ -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 = ({
|
||||
<Input
|
||||
id="routerIP"
|
||||
disabled={!settings.enabled}
|
||||
value={settings.routerIp}
|
||||
onChange={e => onUpdate({ ...settings, routerIp: e.target.value })}
|
||||
value={settings.ip}
|
||||
onChange={e => onUpdate({ ...settings, ip: e.target.value })}
|
||||
invalid={!isRouterIpValid}
|
||||
/>
|
||||
</Col>
|
||||
</FormGroup>
|
||||
<FormGroup row>
|
||||
<Label for="cidr" sm={5}>
|
||||
{t("Prefix length (CIDR)")}
|
||||
</Label>
|
||||
<Col sm={7}>
|
||||
<Input
|
||||
id="cidr"
|
||||
disabled={!settings.enabled}
|
||||
value={settings.cidr === -1 ? "" : settings.cidr}
|
||||
invalid={!isCidrValid}
|
||||
onChange={e => {
|
||||
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 });
|
||||
}}
|
||||
/>
|
||||
</Col>
|
||||
</FormGroup>
|
||||
<FormGroup row>
|
||||
<Label for="localDomain" sm={5}>
|
||||
{t("Local Domain Name")}
|
||||
|
||||
@@ -23,7 +23,13 @@ import ConditionalForwardingSettings, {
|
||||
} from "./ConditionalForwardingSettings";
|
||||
import DnsOptionSettings, { DnsOptionsObject } from "./DnsOptionSettings";
|
||||
import Alert, { AlertType } from "../common/Alert";
|
||||
import { isValidHostname, isValidIpv4 } from "../../util/validate";
|
||||
import {
|
||||
isValidHostname,
|
||||
isValidIpv4,
|
||||
isValidIpv4Cidr,
|
||||
isValidIpv6,
|
||||
isValidIpv6Cidr
|
||||
} from "../../util/validate";
|
||||
|
||||
export interface DNSInfoState {
|
||||
alertMessage: string;
|
||||
@@ -44,8 +50,9 @@ class DNSInfo extends Component<WithTranslation, DNSInfoState> {
|
||||
upstreamDns: [],
|
||||
conditionalForwarding: {
|
||||
enabled: false,
|
||||
routerIp: "",
|
||||
domain: ""
|
||||
ip: "",
|
||||
domain: "",
|
||||
cidr: 24
|
||||
},
|
||||
options: {
|
||||
fqdnRequired: false,
|
||||
@@ -69,8 +76,7 @@ class DNSInfo extends Component<WithTranslation, DNSInfoState> {
|
||||
this.setState({
|
||||
upstreamDns: res.upstream_dns,
|
||||
conditionalForwarding: {
|
||||
enabled: res.conditional_forwarding.enabled,
|
||||
routerIp: res.conditional_forwarding.router_ip,
|
||||
...res.conditional_forwarding,
|
||||
domain
|
||||
},
|
||||
options: {
|
||||
@@ -136,11 +142,7 @@ class DNSInfo extends Component<WithTranslation, DNSInfoState> {
|
||||
this.updateHandler = makeCancelable(
|
||||
api.updateDNSInfo({
|
||||
upstream_dns: this.state.upstreamDns,
|
||||
conditional_forwarding: {
|
||||
enabled: this.state.conditionalForwarding.enabled,
|
||||
router_ip: this.state.conditionalForwarding.routerIp,
|
||||
domain: this.state.conditionalForwarding.domain
|
||||
},
|
||||
conditional_forwarding: this.state.conditionalForwarding,
|
||||
options: {
|
||||
fqdn_required: this.state.options.fqdnRequired,
|
||||
bogus_priv: this.state.options.bogusPriv,
|
||||
@@ -195,8 +197,13 @@ class DNSInfo extends Component<WithTranslation, DNSInfoState> {
|
||||
const { t } = this.props;
|
||||
|
||||
const isRouterIpValid = this.isCFSettingValid(
|
||||
this.state.conditionalForwarding.routerIp,
|
||||
isValidIpv4
|
||||
this.state.conditionalForwarding.ip,
|
||||
address => isValidIpv4(address) || isValidIpv6(address)
|
||||
);
|
||||
|
||||
const isCidrValid = this.isCFSettingValid(
|
||||
this.state.conditionalForwarding.cidr.toString(),
|
||||
cidr => isValidIpv4Cidr(cidr) || isValidIpv6Cidr(cidr)
|
||||
);
|
||||
|
||||
const isDomainValid = this.isCFSettingValid(
|
||||
@@ -230,6 +237,7 @@ class DNSInfo extends Component<WithTranslation, DNSInfoState> {
|
||||
settings={this.state.conditionalForwarding}
|
||||
onUpdate={this.handleConditionalForwardingUpdate}
|
||||
isRouterIpValid={isRouterIpValid}
|
||||
isCidrValid={isCidrValid}
|
||||
isDomainValid={isDomainValid}
|
||||
t={t}
|
||||
/>
|
||||
@@ -243,7 +251,12 @@ class DNSInfo extends Component<WithTranslation, DNSInfoState> {
|
||||
</FormGroup>
|
||||
<Button
|
||||
type="submit"
|
||||
disabled={this.state.processing || !isRouterIpValid || !isDomainValid}
|
||||
disabled={
|
||||
this.state.processing ||
|
||||
!isRouterIpValid ||
|
||||
!isCidrValid ||
|
||||
!isDomainValid
|
||||
}
|
||||
>
|
||||
{t("Apply")}
|
||||
</Button>
|
||||
|
||||
Vendored
+4
-3
@@ -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<ApiClient>;
|
||||
top_clients: Array<ApiClientData>;
|
||||
total_queries: number;
|
||||
}
|
||||
|
||||
@@ -197,7 +198,7 @@ interface ApiTopBlockedClients {
|
||||
blocked_queries: number;
|
||||
}
|
||||
|
||||
interface ApiClient {
|
||||
interface ApiClientData {
|
||||
name: string;
|
||||
ip: string;
|
||||
count: number;
|
||||
|
||||
@@ -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);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user