From 8f84a66dbcb1f9bc370f09d118cd262d2b8052c5 Mon Sep 17 00:00:00 2001 From: Mcat12 Date: Thu, 29 Nov 2018 17:13:53 -0500 Subject: [PATCH 01/14] Add isValidIpv4OptionalPort validation function Signed-off-by: Mcat12 --- src/validate.js | 26 ++++++++++++++++++++++++-- src/validate.test.js | 35 +++++++++++++++++++++++++++++++++++ 2 files changed, 59 insertions(+), 2 deletions(-) diff --git a/src/validate.js b/src/validate.js index 2bb61ce..dfcffad 100644 --- a/src/validate.js +++ b/src/validate.js @@ -65,10 +65,32 @@ export function isValidIpv4(address) { return false; } - // No segment can be longer than 3 characters or be empty - // All segments must be numbers (positive) return segments.every( segment => isPositiveNumber(segment) && parseInt(segment) < 256 ); } + +/** + * Check if the string is a valid IPv4 address, and it can contain an optional + * port after the address, separated with a #. + * Example: 127.0.0.1#5353 + * + * @param address {string} the address to check + * @returns {boolean} if the address is a valid IPv4 address and the port + * (if it exists) is valid. + */ +export function isValidIpv4OptionalPort(address) { + const split = address.split("#"); + const ipv4 = split[0]; + + // Check the IPv4 address + if (!isValidIpv4(ipv4)) { + return false; + } + + // If no port is given or if the port is valid, return true + return ( + split.length === 1 || (split.length === 2 && isPositiveNumber(split[1])) + ); +} diff --git a/src/validate.test.js b/src/validate.test.js index 17eda8b..4c46022 100644 --- a/src/validate.test.js +++ b/src/validate.test.js @@ -12,6 +12,7 @@ import { isPositiveNumber, isValidDomain, isValidIpv4, + isValidIpv4OptionalPort, isValidRegex } from "./validate"; @@ -188,4 +189,38 @@ describe("Testing the validation functions", () => { expect(isValidIpv4("555.666.777.888")).toBe(false); }); }); + + describe("isValidIpv4OptionalPort", () => { + it("passes 127.0.0.1", () => { + expect(isValidIpv4OptionalPort("127.0.0.1")).toBe(true); + }); + + it("passes 127.0.0.1#53", () => { + expect(isValidIpv4OptionalPort("127.0.0.1#53")).toBe(true); + }); + + it("passes 8.8.8.8#5353", () => { + expect(isValidIpv4OptionalPort("8.8.8.8#5353")).toBe(true); + }); + + it("fails 1111.1.1.1#53", () => { + expect(isValidIpv4OptionalPort("1111.1.1.1#53")).toBe(false); + }); + + it("fails 8.8.8.8#", () => { + expect(isValidIpv4OptionalPort("8.8.8.8#")).toBe(false); + }); + + it("fails 8.8.8.8##", () => { + expect(isValidIpv4OptionalPort("8.8.8.8##")).toBe(false); + }); + + it("fails 8.8.8.8#53#", () => { + expect(isValidIpv4OptionalPort("8.8.8.8#53#")).toBe(false); + }); + + it("fails 8.8.8.8#abc", () => { + expect(isValidIpv4OptionalPort("8.8.8.8#abc")).toBe(false); + }); + }); }); From b0a377f80c00a0efddb898656efd8497536fbf35 Mon Sep 17 00:00:00 2001 From: Mcat12 Date: Fri, 30 Nov 2018 17:36:45 -0500 Subject: [PATCH 02/14] Implement DnsList components These components will display an editable list of upstream DNS servers. Signed-off-by: Mcat12 --- src/components/settings/DnsList.js | 50 +++++++++++++++++++ src/components/settings/DnsListItem.js | 29 +++++++++++ src/components/settings/DnsListNewItem.js | 60 +++++++++++++++++++++++ 3 files changed, 139 insertions(+) create mode 100644 src/components/settings/DnsList.js create mode 100644 src/components/settings/DnsListItem.js create mode 100644 src/components/settings/DnsListNewItem.js diff --git a/src/components/settings/DnsList.js b/src/components/settings/DnsList.js new file mode 100644 index 0000000..5ac16a3 --- /dev/null +++ b/src/components/settings/DnsList.js @@ -0,0 +1,50 @@ +/* Pi-hole: A black hole for Internet advertisements +* (c) 2017 Pi-hole, LLC (https://pi-hole.net) +* Network-wide ad blocking via your own hardware. +* +* Web Interface +* An Editable List of Upstream DNS Servers +* +* 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 PropTypes from "prop-types"; +import { ListGroup } from "reactstrap"; +import DnsListItem from "./DnsListItem"; +import DnsListNewItem from "./DnsListNewItem"; +import { isValidIpv4OptionalPort } from "../../validate"; + +/** + * Check if an upstream address is unique and valid + * + * @param address the address to check + * @param upstreams the list of current upstreams + * @returns {boolean} if the upstream address is unique and valid + */ +export const isAddressValid = (address, upstreams) => + !upstreams.includes(address) && isValidIpv4OptionalPort(address); + +const DnsList = ({ upstreams, onAdd, onRemove }) => ( + + {upstreams.map(upstream => ( + onRemove(upstream)} + address={upstream} + /> + ))} + isAddressValid(address, upstreams)} + /> + +); + +DnsList.propTypes = { + upstreams: PropTypes.arrayOf(PropTypes.string).isRequired, + onAdd: PropTypes.func.isRequired, + onRemove: PropTypes.func.isRequired +}; + +export default DnsList; diff --git a/src/components/settings/DnsListItem.js b/src/components/settings/DnsListItem.js new file mode 100644 index 0000000..276c50c --- /dev/null +++ b/src/components/settings/DnsListItem.js @@ -0,0 +1,29 @@ +/* Pi-hole: A black hole for Internet advertisements +* (c) 2017 Pi-hole, LLC (https://pi-hole.net) +* Network-wide ad blocking via your own hardware. +* +* Web Interface +* A Upstream DNS List Item +* +* 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 PropTypes from "prop-types"; +import { Button, ListGroupItem } from "reactstrap"; + +const DnsListItem = ({ address, onRemove }) => ( + + {address} + + +); + +DnsListItem.propTypes = { + address: PropTypes.string.isRequired, + onRemove: PropTypes.func.isRequired +}; + +export default DnsListItem; diff --git a/src/components/settings/DnsListNewItem.js b/src/components/settings/DnsListNewItem.js new file mode 100644 index 0000000..173545b --- /dev/null +++ b/src/components/settings/DnsListNewItem.js @@ -0,0 +1,60 @@ +/* Pi-hole: A black hole for Internet advertisements +* (c) 2017 Pi-hole, LLC (https://pi-hole.net) +* Network-wide ad blocking via your own hardware. +* +* Web Interface +* Input For New DNS List Items +* +* This file is copyright under the latest version of the EUPL. +* Please see LICENSE file for your rights under this license. */ + +import React, { Component } from "react"; +import PropTypes from "prop-types"; +import { + Button, + Input, + InputGroup, + InputGroupAddon, + ListGroupItem +} from "reactstrap"; + +export default class DnsListNewItem extends Component { + static propTypes = { + onAdd: PropTypes.func.isRequired, + isValid: PropTypes.func.isRequired + }; + + state = { + address: "" + }; + + render() { + const isAddressValid = this.props.isValid(this.state.address); + + return ( + + + this.setState({ address: e.target.value })} + /> + + + + + + ); + } +} From cc9220509fc4c5eca36b17a24c08a6d14a21620e Mon Sep 17 00:00:00 2001 From: Mcat12 Date: Fri, 30 Nov 2018 17:37:45 -0500 Subject: [PATCH 03/14] Make the DNS settings editable Signed-off-by: Mcat12 --- src/components/settings/DNSInfo.js | 202 +++++++++++++++++++++++------ 1 file changed, 161 insertions(+), 41 deletions(-) diff --git a/src/components/settings/DNSInfo.js b/src/components/settings/DNSInfo.js index 7e7c278..00b4fdc 100644 --- a/src/components/settings/DNSInfo.js +++ b/src/components/settings/DNSInfo.js @@ -12,6 +12,8 @@ import React, { Component } from "react"; import { translate } from "react-i18next"; import { ignoreCancel, makeCancelable } from "../../util"; import api from "../../util/api"; +import DnsList from "./DnsList"; +import { Button, Col, Form, FormGroup, Input, Label } from "reactstrap"; class DNSInfo extends Component { state = { @@ -54,51 +56,169 @@ class DNSInfo extends Component { this.updateHandler.cancel(); } + handleUpstreamAdd = upstream => { + this.setState({ upstream_dns: this.state.upstream_dns.concat(upstream) }); + }; + + handleUpstreamRemove = upstream => { + this.setState({ + upstream_dns: this.state.upstream_dns.filter(item => item !== upstream) + }); + }; + render() { const { t } = this.props; return ( -
-
-
-            
- {t("Upstream DNS Servers")}:
- {this.state.upstream_dns.map(item => item + "\n")} -
-
-
-
-
-            
- {t("Interface listening behavior")}:{" "} - {this.state.options.listening_type} -
- {t("Forward FQDNs only")}:{" "} - {this.state.options.fqdn_required.toString()} -
- {t("Only forward public reverse lookups")}: - {this.state.options.bogus_priv.toString()} -
- {t("Use DNSSEC")}: {this.state.options.dnssec.toString()} -
-
-
-
-            
- {t("Conditional Forwarding")} -
- {t("Enabled")}:{" "} - {this.state.conditional_forwarding.enabled.toString()} -
- {t("Router IP")}:{" "} - {this.state.conditional_forwarding.router_ip.toString()} -
- {t("Local Domain Name")}:{" "} - {this.state.conditional_forwarding.domain.toString()} -
-
-
-
+
+ + +

{t("Upstream DNS Servers")}

+ + + +

{t("Conditional Forwarding")}

+ + + + + + + + this.setState({ + conditional_forwarding: { + ...this.state.conditional_forwarding, + router_ip: e.target.value + } + }) + } + /> + + + + + + + this.setState({ + conditional_forwarding: { + ...this.state.conditional_forwarding, + domain: e.target.value + } + }) + } + /> + + +

{t("DNS Options")}

+ + + + + this.setState({ + options: { + ...this.state.options, + listening_type: e.target.value + } + }) + } + > + + + + + + + + + + + + + + + + +
+ +
); } } From 8b520914b05f9213f504896029d6d6dbf07f87a0 Mon Sep 17 00:00:00 2001 From: Mcat12 Date: Fri, 30 Nov 2018 17:38:38 -0500 Subject: [PATCH 04/14] Fix listening type fake data "lan" should be "local" Signed-off-by: Mcat12 --- make-fake-data.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/make-fake-data.js b/make-fake-data.js index e2e9dc9..7ef11c4 100644 --- a/make-fake-data.js +++ b/make-fake-data.js @@ -294,7 +294,7 @@ function getDNSInfo() { "fqdn_required": faker.random.boolean(), "bogus_priv": faker.random.boolean(), "dnssec": faker.random.boolean(), - "listening_type": faker.random.arrayElement(["single", "lan", "all"]), + "listening_type": faker.random.arrayElement(["single", "local", "all"]), }, "conditional_forwarding": { "enabled": faker.random.boolean(), From c451232c9e1f265d9e54f5f621c7260ff1864482 Mon Sep 17 00:00:00 2001 From: Mcat12 Date: Tue, 11 Dec 2018 20:52:19 -0500 Subject: [PATCH 05/14] Fix checkbox input margin Now the checkboxes line up with the labels. Signed-off-by: Mcat12 --- src/scss/_custom.scss | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/scss/_custom.scss b/src/scss/_custom.scss index 5548db6..2b3feb9 100644 --- a/src/scss/_custom.scss +++ b/src/scss/_custom.scss @@ -182,3 +182,7 @@ .form-check-label { margin-bottom: 10px; } + +.form-check-input { + margin-top: 0.2rem; +} From ef860c576393e45d0f8fc8097318914ebb918e89 Mon Sep 17 00:00:00 2001 From: Mcat12 Date: Wed, 12 Dec 2018 15:10:48 -0500 Subject: [PATCH 06/14] Prevent the page from reloading when submitting the DNS form Signed-off-by: Mcat12 --- src/components/settings/DNSInfo.js | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/src/components/settings/DNSInfo.js b/src/components/settings/DNSInfo.js index 00b4fdc..b97794a 100644 --- a/src/components/settings/DNSInfo.js +++ b/src/components/settings/DNSInfo.js @@ -66,11 +66,15 @@ class DNSInfo extends Component { }); }; + saveSettings = e => { + e.preventDefault(); + }; + render() { const { t } = this.props; return ( -
+

{t("Upstream DNS Servers")}

From 6d191a1b1a3059a95b484bb96c854f20721c5f10 Mon Sep 17 00:00:00 2001 From: Mcat12 Date: Thu, 13 Dec 2018 14:07:06 -0500 Subject: [PATCH 07/14] Refactor DNS settings to split out sections into new components Also decoupled the state a bit from the API output. Signed-off-by: Mcat12 --- .../settings/ConditionalForwardingSettings.js | 62 ++++++ src/components/settings/DNSInfo.js | 191 +++++------------- src/components/settings/DnsOptionSettings.js | 77 +++++++ 3 files changed, 186 insertions(+), 144 deletions(-) create mode 100644 src/components/settings/ConditionalForwardingSettings.js create mode 100644 src/components/settings/DnsOptionSettings.js diff --git a/src/components/settings/ConditionalForwardingSettings.js b/src/components/settings/ConditionalForwardingSettings.js new file mode 100644 index 0000000..03a1a5f --- /dev/null +++ b/src/components/settings/ConditionalForwardingSettings.js @@ -0,0 +1,62 @@ +/* Pi-hole: A black hole for Internet advertisements +* (c) 2017 Pi-hole, LLC (https://pi-hole.net) +* Network-wide ad blocking via your own hardware. +* +* Web Interface +* Settings :: DNS :: Upstream DNS Servers +* +* This file is copyright under the latest version of the EUPL. +* Please see LICENSE file for your rights under this license. */ + +import React, { Fragment } from "react"; +import PropTypes from "prop-types"; +import { Col, FormGroup, Input, Label } from "reactstrap"; + +const ConditionalForwardingSettings = ({ settings, onUpdate, t }) => ( + + + + + + + + onUpdate({ ...settings, routerIp: e.target.value })} + /> + + + + + + onUpdate({ ...settings, domain: e.target.value })} + /> + + + +); + +ConditionalForwardingSettings.propTypes = { + settings: PropTypes.object.isRequired, + onUpdate: PropTypes.func.isRequired, + t: PropTypes.func.isRequired +}; + +export default ConditionalForwardingSettings; diff --git a/src/components/settings/DNSInfo.js b/src/components/settings/DNSInfo.js index b97794a..c00f451 100644 --- a/src/components/settings/DNSInfo.js +++ b/src/components/settings/DNSInfo.js @@ -3,7 +3,7 @@ * Network-wide ad blocking via your own hardware. * * Web Interface -* Settings :: DHCP +* Settings :: DNS * * This file is copyright under the latest version of the EUPL. * Please see LICENSE file for your rights under this license. */ @@ -13,21 +13,23 @@ import { translate } from "react-i18next"; import { ignoreCancel, makeCancelable } from "../../util"; import api from "../../util/api"; import DnsList from "./DnsList"; -import { Button, Col, Form, FormGroup, Input, Label } from "reactstrap"; +import { Button, Col, Form, FormGroup } from "reactstrap"; +import ConditionalForwardingSettings from "./ConditionalForwardingSettings"; +import DnsOptionSettings from "./DnsOptionSettings"; class DNSInfo extends Component { state = { - upstream_dns: [], - conditional_forwarding: { + upstreamDns: [], + conditionalForwarding: { enabled: false, - router_ip: "", + routerIp: "", domain: "" }, options: { - fqdn_required: false, - bogus_priv: false, + fqdnRequired: false, + bogusPriv: false, dnssec: false, - listening_type: "single" + listeningType: "single" } }; @@ -43,7 +45,20 @@ class DNSInfo extends Component { }); this.updateHandler.promise .then(res => { - this.setState(res); + this.setState({ + upstreamDns: res.upstream_dns, + conditionalForwarding: { + enabled: res.conditional_forwarding.enabled, + routerIp: res.conditional_forwarding.router_ip, + domain: res.conditional_forwarding.domain + }, + options: { + fqdnRequired: res.options.fqdn_required, + bogusPriv: res.options.bogus_priv, + dnssec: res.options.dnssec, + listeningType: res.options.listening_type + } + }); }) .catch(ignoreCancel); } @@ -57,15 +72,25 @@ class DNSInfo extends Component { } handleUpstreamAdd = upstream => { - this.setState({ upstream_dns: this.state.upstream_dns.concat(upstream) }); + this.setState({ + upstreamDns: this.state.upstreamDns.concat(upstream) + }); }; handleUpstreamRemove = upstream => { this.setState({ - upstream_dns: this.state.upstream_dns.filter(item => item !== upstream) + upstreamDns: this.state.upstreamDns.filter(item => item !== upstream) }); }; + handleConditionalForwardingUpdate = conditionalForwarding => { + this.setState({ conditionalForwarding }); + }; + + handleDnsOptionsUpdate = options => { + this.setState({ options }); + }; + saveSettings = e => { e.preventDefault(); }; @@ -81,144 +106,22 @@ class DNSInfo extends Component {

{t("Conditional Forwarding")}

- - - - - - - - this.setState({ - conditional_forwarding: { - ...this.state.conditional_forwarding, - router_ip: e.target.value - } - }) - } - /> - - - - - - - this.setState({ - conditional_forwarding: { - ...this.state.conditional_forwarding, - domain: e.target.value - } - }) - } - /> - - +

{t("DNS Options")}

- - - - - this.setState({ - options: { - ...this.state.options, - listening_type: e.target.value - } - }) - } - > - - - - - - - - - - - - - - - +
diff --git a/src/components/settings/DnsOptionSettings.js b/src/components/settings/DnsOptionSettings.js new file mode 100644 index 0000000..782d58f --- /dev/null +++ b/src/components/settings/DnsOptionSettings.js @@ -0,0 +1,77 @@ +/* Pi-hole: A black hole for Internet advertisements +* (c) 2017 Pi-hole, LLC (https://pi-hole.net) +* Network-wide ad blocking via your own hardware. +* +* Web Interface +* Settings :: DNS :: DNS Options +* +* This file is copyright under the latest version of the EUPL. +* Please see LICENSE file for your rights under this license. */ + +import React, { Fragment } from "react"; +import PropTypes from "prop-types"; +import { Col, FormGroup, Input, Label } from "reactstrap"; + +const DnsOptionSettings = ({ settings, onUpdate, t }) => ( + + + + + + onUpdate({ ...settings, listeningType: e.target.value }) + } + > + + + + + + + + + + + + + + + + +); + +DnsOptionSettings.propTypes = { + settings: PropTypes.object.isRequired, + onUpdate: PropTypes.func.isRequired, + t: PropTypes.func.isRequired +}; + +export default DnsOptionSettings; From dc3c9f064028a80b22d516717cf53e9a78796d0e Mon Sep 17 00:00:00 2001 From: Mcat12 Date: Thu, 13 Dec 2018 16:34:13 -0500 Subject: [PATCH 08/14] Add recommended servers to the upstream DNS input This uses a new package: react-bootstrap-typeahead. You can switch between the custom and recommended input types via a toggle button. Signed-off-by: Mcat12 --- package-lock.json | 117 +++++++++++++++-- package.json | 7 +- src/components/settings/DnsList.js | 1 + src/components/settings/DnsListNewItem.js | 70 ++++++++-- .../settings/recommendedUpstreams.js | 121 ++++++++++++++++++ src/index.js | 1 + 6 files changed, 295 insertions(+), 22 deletions(-) create mode 100644 src/components/settings/recommendedUpstreams.js diff --git a/package-lock.json b/package-lock.json index 9536876..1c38b83 100644 --- a/package-lock.json +++ b/package-lock.json @@ -4004,6 +4004,15 @@ "sha.js": "^2.4.8" } }, + "create-react-context": { + "version": "0.2.2", + "resolved": "https://registry.npmjs.org/create-react-context/-/create-react-context-0.2.2.tgz", + "integrity": "sha512-KkpaLARMhsTsgp0d2NA/R94F/eDLbhXERdIq3LvX2biCAXcDvHYoOqHfWCHf1+OLj+HKBotLG3KqaOOf+C1C+A==", + "requires": { + "fbjs": "^0.8.0", + "gud": "^1.0.0" + } + }, "cross-spawn": { "version": "3.0.1", "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-3.0.1.tgz", @@ -5252,8 +5261,7 @@ "escape-string-regexp": { "version": "1.0.5", "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz", - "integrity": "sha1-G2HAViGQqN/2rjuyzwIAyhMLhtQ=", - "dev": true + "integrity": "sha1-G2HAViGQqN/2rjuyzwIAyhMLhtQ=" }, "escodegen": { "version": "1.9.1", @@ -7224,6 +7232,11 @@ "integrity": "sha1-8QdIy+dq+WS3yWyTxrzCivEgwIE=", "dev": true }, + "gud": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/gud/-/gud-1.0.0.tgz", + "integrity": "sha512-zGEOVKFM5sVPPrYs7J5/hYEw2Pof8KCyOwyhG8sAF26mCAeUFAcYPu1mwB7hhpIP29zOIBaDqwuHdLp0jvZXjw==" + }, "gzip-size": { "version": "5.0.0", "resolved": "https://registry.npmjs.org/gzip-size/-/gzip-size-5.0.0.tgz", @@ -11317,10 +11330,9 @@ } }, "merge": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/merge/-/merge-1.2.0.tgz", - "integrity": "sha1-dTHjnUlJwoGma4xabgJl6LBYlNo=", - "dev": true + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/merge/-/merge-1.2.1.tgz", + "integrity": "sha512-VjFo4P5Whtj4vsLzsYBu5ayHhoHJ0UqNm7ibvShmbmoz7tGi0vXaoJbGdB+GmDMLUdg8DpQXEIeVDAe8MaABvQ==" }, "merge-deep": { "version": "3.0.2", @@ -17753,6 +17765,15 @@ } } }, + "prop-types-extra": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/prop-types-extra/-/prop-types-extra-1.1.0.tgz", + "integrity": "sha512-QFyuDxvMipmIVKD2TwxLVPzMnO4e5oOf1vr3tJIomL8E7d0lr6phTHd5nkPhFIzTD1idBLLEPeylL9g+rrTzRg==", + "requires": { + "react-is": "^16.3.2", + "warning": "^3.0.0" + } + }, "proxy-addr": { "version": "2.0.4", "resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.4.tgz", @@ -18034,6 +18055,64 @@ } } }, + "react-bootstrap-typeahead": { + "version": "3.2.4", + "resolved": "https://registry.npmjs.org/react-bootstrap-typeahead/-/react-bootstrap-typeahead-3.2.4.tgz", + "integrity": "sha512-wFXIKbOHY4Q1AtnhSQIAnUvSDxmcFdDc5wIX/yd7YJug2jL5TvKspJ3XfX/FaWEWyn1pgHukRuFGnbYnL8rtUQ==", + "requires": { + "classnames": "^2.2.0", + "escape-string-regexp": "^1.0.5", + "invariant": "^2.2.1", + "lodash": "^4.17.2", + "prop-types": "^15.5.8", + "prop-types-extra": "^1.0.1", + "react-onclickoutside": "^6.1.1", + "react-overlays": "^0.8.1", + "react-popper": "^1.0.0", + "warning": "^4.0.1" + }, + "dependencies": { + "@babel/runtime": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.2.0.tgz", + "integrity": "sha512-oouEibCbHMVdZSDlJBO6bZmID/zA/G/Qx3H1d3rSNPTD+L8UNKvCat7aKWSJ74zYbm5zWGh0GQN0hKj8zYFTCg==", + "requires": { + "regenerator-runtime": "^0.12.0" + } + }, + "popper.js": { + "version": "1.14.6", + "resolved": "https://registry.npmjs.org/popper.js/-/popper.js-1.14.6.tgz", + "integrity": "sha512-AGwHGQBKumlk/MDfrSOf0JHhJCImdDMcGNoqKmKkU+68GFazv3CQ6q9r7Ja1sKDZmYWTckY/uLyEznheTDycnA==" + }, + "react-popper": { + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/react-popper/-/react-popper-1.3.2.tgz", + "integrity": "sha512-UbFWj55Yt9uqvy0oZ+vULDL2Bw1oxeZF9/JzGyxQ5ypgauRH/XlarA5+HLZWro/Zss6Ht2kqpegtb6sYL8GUGw==", + "requires": { + "@babel/runtime": "^7.1.2", + "create-react-context": "<=0.2.2", + "popper.js": "^1.14.4", + "prop-types": "^15.6.1", + "typed-styles": "^0.0.7", + "warning": "^4.0.2" + } + }, + "regenerator-runtime": { + "version": "0.12.1", + "resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.12.1.tgz", + "integrity": "sha512-odxIc1/vDlo4iZcfXqRYFj0vpXFNoGdKMAUieAlFYO6m/nl5e9KR/beGf41z4a1FI+aQgtjhuaSlDxQ0hmkrHg==" + }, + "warning": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/warning/-/warning-4.0.2.tgz", + "integrity": "sha512-wbTp09q/9C+jJn4KKJfJfoS6VleK/Dti0yqWSm6KMvJ4MRCXFQNapHuJXutJIrWV0Cf4AhTdeIe4qdKHR1+Hug==", + "requires": { + "loose-envify": "^1.0.0" + } + } + } + }, "react-chartjs-2": { "version": "2.7.2", "resolved": "https://registry.npmjs.org/react-chartjs-2/-/react-chartjs-2-2.7.2.tgz", @@ -18294,8 +18373,25 @@ "react-is": { "version": "16.4.1", "resolved": "https://registry.npmjs.org/react-is/-/react-is-16.4.1.tgz", - "integrity": "sha512-xpb0PpALlFWNw/q13A+1aHeyJyLYCg0/cCHPUA43zYluZuIPHaHL3k8OBsTgQtxqW0FhyDEMvi8fZ/+7+r4OSQ==", - "dev": true + "integrity": "sha512-xpb0PpALlFWNw/q13A+1aHeyJyLYCg0/cCHPUA43zYluZuIPHaHL3k8OBsTgQtxqW0FhyDEMvi8fZ/+7+r4OSQ==" + }, + "react-onclickoutside": { + "version": "6.7.1", + "resolved": "https://registry.npmjs.org/react-onclickoutside/-/react-onclickoutside-6.7.1.tgz", + "integrity": "sha512-p84kBqGaMoa7VYT0vZ/aOYRfJB+gw34yjpda1Z5KeLflg70HipZOT+MXQenEhdkPAABuE2Astq4zEPdMqUQxcg==" + }, + "react-overlays": { + "version": "0.8.3", + "resolved": "https://registry.npmjs.org/react-overlays/-/react-overlays-0.8.3.tgz", + "integrity": "sha512-h6GT3jgy90PgctleP39Yu3eK1v9vaJAW73GOA/UbN9dJ7aAN4BTZD6793eI1D5U+ukMk17qiqN/wl3diK1Z5LA==", + "requires": { + "classnames": "^2.2.5", + "dom-helpers": "^3.2.1", + "prop-types": "^15.5.10", + "prop-types-extra": "^1.0.1", + "react-transition-group": "^2.2.0", + "warning": "^3.0.0" + } }, "react-perfect-scrollbar": { "version": "1.1.1", @@ -21286,6 +21382,11 @@ } } }, + "typed-styles": { + "version": "0.0.7", + "resolved": "https://registry.npmjs.org/typed-styles/-/typed-styles-0.0.7.tgz", + "integrity": "sha512-pzP0PWoZUhsECYjABgCGQlRGL1n7tOHsgwYv3oIiEpJwGhFTuty/YNeduxQYzXXa3Ge5BdT6sHYIQYpl4uJ+5Q==" + }, "typedarray": { "version": "0.0.6", "resolved": "https://registry.npmjs.org/typedarray/-/typedarray-0.0.6.tgz", diff --git a/package.json b/package.json index 0ddb3bf..fa37f42 100644 --- a/package.json +++ b/package.json @@ -25,14 +25,14 @@ "@coreui/react": "^2.0.4", "bootstrap": "^4.1.1", "chart.js": "^2.7.2", + "font-awesome": "^4.7.0", "i18next": "^11.3.2", "i18next-browser-languagedetector": "^2.2.0", "i18next-xhr-backend": "^1.5.1", "ionicons": "^3.0.0", - "font-awesome": "^4.7.0", - "simple-line-icons": "^2.4.1", "prop-types": "^15.6.1", "react": "^16.4.1", + "react-bootstrap-typeahead": "^3.2.4", "react-chartjs-2": "^2.7.2", "react-dom": "^16.4.1", "react-i18next": "^7.6.1", @@ -40,7 +40,8 @@ "react-table": "^6.8.2", "react-transition-group": "^2.3.1", "reactstrap": "^6.1.0", - "sha.js": "^2.4.11" + "sha.js": "^2.4.11", + "simple-line-icons": "^2.4.1" }, "scripts": { "start": "npm run build-css && npm-run-all -p watch-css start-js", diff --git a/src/components/settings/DnsList.js b/src/components/settings/DnsList.js index 5ac16a3..7beaee2 100644 --- a/src/components/settings/DnsList.js +++ b/src/components/settings/DnsList.js @@ -37,6 +37,7 @@ const DnsList = ({ upstreams, onAdd, onRemove }) => ( isAddressValid(address, upstreams)} + upstreams={upstreams} /> ); diff --git a/src/components/settings/DnsListNewItem.js b/src/components/settings/DnsListNewItem.js index 173545b..0573d7b 100644 --- a/src/components/settings/DnsListNewItem.js +++ b/src/components/settings/DnsListNewItem.js @@ -17,36 +17,84 @@ import { InputGroupAddon, ListGroupItem } from "reactstrap"; +import { Typeahead } from "react-bootstrap-typeahead"; +import { recommendedUpstreamOptions } from "./recommendedUpstreams"; +/** + * A component to add upstream DNS servers. The servers are either selected from + * a list of recommended upstreams, or custom servers can be input. + */ export default class DnsListNewItem extends Component { static propTypes = { onAdd: PropTypes.func.isRequired, - isValid: PropTypes.func.isRequired + isValid: PropTypes.func.isRequired, + upstreams: PropTypes.arrayOf(PropTypes.string).isRequired }; state = { - address: "" + address: "", + selected: [], + isCustom: false }; render() { - const isAddressValid = this.props.isValid(this.state.address); + // Check if input is valid. If it's a custom server, validate the IP + // address. If we're choosing a recommended server, just make sure one is + // selected. + const isAddressValid = this.state.isCustom + ? this.props.isValid(this.state.address) + : this.state.selected.length !== 0; + + // The component used for custom input + const customInput = ( + this.setState({ address: e.target.value })} + /> + ); + + // The component used for selecting a recommended server + const recommendedInput = ( + { + this.setState({ selected }); + }} + options={recommendedUpstreamOptions.filter( + upstream => !this.props.upstreams.includes(upstream.address) + )} + selected={this.state.selected} + bodyContainer + /> + ); return ( - this.setState({ address: e.target.value })} - /> + {this.state.isCustom ? customInput : recommendedInput} + + + + + - + this.setState({ address })} + onChange={selected => this.setState({ selected })} + options={recommendedUpstreamOptions.filter( + upstream => !this.props.upstreams.includes(upstream.address) + )} + selected={this.state.selected} + bodyContainer + /> + ); } diff --git a/src/util/api.js b/src/util/api.js index 60b4251..b7d91f5 100644 --- a/src/util/api.js +++ b/src/util/api.js @@ -84,5 +84,8 @@ export default { }, updateDHCPInfo(settings) { return http.put("settings/dhcp", settings); + }, + updateDNSInfo(settings) { + return http.put("settings/dns", settings); } }; From 2e7c37b7fdce3a40d4e13f74874777305f07ee35 Mon Sep 17 00:00:00 2001 From: Mcat12 Date: Sun, 16 Dec 2018 11:55:01 -0500 Subject: [PATCH 14/14] Fill in a default conditional forwarding domain if it is empty Signed-off-by: Mcat12 --- src/components/settings/DNSInfo.js | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/src/components/settings/DNSInfo.js b/src/components/settings/DNSInfo.js index 0777c28..3aa5cc6 100644 --- a/src/components/settings/DNSInfo.js +++ b/src/components/settings/DNSInfo.js @@ -43,12 +43,16 @@ class DNSInfo extends Component { this.loadHandler = makeCancelable(api.getDNSInfo()); this.loadHandler.promise .then(res => { + // If the domain is empty, fill it in with a default. + const savedDomain = res.conditional_forwarding.domain; + const domain = savedDomain.length === 0 ? "lan" : savedDomain; + this.setState({ upstreamDns: res.upstream_dns, conditionalForwarding: { enabled: res.conditional_forwarding.enabled, routerIp: res.conditional_forwarding.router_ip, - domain: res.conditional_forwarding.domain + domain }, options: { fqdnRequired: res.options.fqdn_required,