Merge branch 'development' into feature/with-api-component

This commit is contained in:
Mcat12
2018-11-10 11:15:35 -05:00
14 changed files with 296 additions and 58 deletions
+1
View File
@@ -0,0 +1 @@
{}
+1
View File
@@ -0,0 +1 @@
{}
+1
View File
@@ -0,0 +1 @@
{}
+22
View File
@@ -0,0 +1,22 @@
{
"unknown": "Unknown error",
"gravity_error": "Failed to create the blocklist",
"ftl_connection_fail": "Failed to connect to FTL",
"ftl_read_error": "Error reading from FTL",
"ftl_eom_error": "Read unexpected EOM from FTL",
"not_found": "Not found",
"already_exists": "Item already exists",
"invalid_domain": "Invalid domain",
"bad_request": "Bad request",
"unauthorized": "Unauthorized",
"file_read": "Error reading from {{file}}",
"file_write": "Error writing to {{file}}",
"config_parsing_error": "Error parsing the config",
"invalid_setting_value": "Invalid setting value",
"restart_dns_error": "Failed to restart the DNS server",
"dnsmasq_config_write": "Error generating the dnsmasq config",
"shared_memory_open": "Failed to open shared memory",
"shared_memory_read": "Failed to read from shared memory",
"shared_memory_lock": "Failed to lock shared memory",
"API Error: {{error}}": "API Error: {{error}}"
}
+4 -1
View File
@@ -22,5 +22,8 @@
"Branch": "Branch",
"Hash": "Hash",
"Tag": "Tag",
"Only forward public reverse lookups": "Only forward public reverse lookups"
"Only forward public reverse lookups": "Only forward public reverse lookups",
"Processing...": "Processing...",
"Successfully saved settings": "Successfully saved settings",
"Apply": "Apply"
}
+1
View File
@@ -0,0 +1 @@
{}
+1
View File
@@ -0,0 +1 @@
{}
+1
View File
@@ -0,0 +1 @@
{}
+5 -1
View File
@@ -27,7 +27,11 @@ const Alert = props => {
Alert.propTypes = {
message: PropTypes.node.isRequired,
type: PropTypes.string.isRequired,
onClick: PropTypes.func.isRequired
onClick: PropTypes.func
};
Alert.defaultProps = {
onClick: () => {}
};
export default Alert;
+115 -52
View File
@@ -23,45 +23,46 @@ import {
Label
} from "reactstrap";
import { isPositiveNumber, isValidHostname, isValidIpv4 } from "../../validate";
import Alert from "../common/Alert";
class DHCPInfo extends Component {
state = {
active: false,
ip_start: "",
ip_end: "",
router_ip: "",
lease_time: "",
domain: "",
ipv6_support: false
alertMessage: "",
alertType: "",
showAlert: false,
processing: false,
settings: {
active: false,
ip_start: "",
ip_end: "",
router_ip: "",
lease_time: "",
domain: "",
ipv6_support: false
}
};
constructor(props) {
super(props);
this.updateDHCPInfo = this.updateDHCPInfo.bind(this);
}
updateDHCPInfo() {
this.updateHandler = makeCancelable(api.getDHCPInfo(), {
repeat: this.updateDHCPInfo,
interval: 600000
});
loadDHCPInfo = () => {
this.updateHandler = makeCancelable(api.getDHCPInfo());
this.updateHandler.promise
.then(res => {
this.setState({
active: res.active,
ip_start: res.ip_start,
ip_end: res.ip_end,
router_ip: res.router_ip,
lease_time: res.lease_time,
domain: res.domain,
ipv6_support: res.ipv6_support
settings: {
active: res.active,
ip_start: res.ip_start,
ip_end: res.ip_end,
router_ip: res.router_ip,
lease_time: res.lease_time,
domain: res.domain,
ipv6_support: res.ipv6_support
}
});
})
.catch(ignoreCancel);
}
};
componentDidMount() {
this.updateDHCPInfo();
this.loadDHCPInfo();
}
componentWillUnmount() {
@@ -77,10 +78,16 @@ class DHCPInfo extends Component {
* @returns {function(Event)}
*/
onChange = (key, attr) => {
return e =>
this.setState({
[key]: e.target[attr]
});
return e => {
const value = e.target[attr];
this.setState(oldState => ({
settings: {
...oldState.settings,
[key]: value
}
}));
};
};
/**
@@ -91,7 +98,44 @@ class DHCPInfo extends Component {
saveSettings = e => {
e.preventDefault();
// TODO: send settings to API
const { t } = this.props;
this.setState({
alertMessage: t("Processing..."),
alertType: "info",
showAlert: true,
processing: true
});
api
.updateDHCPInfo(this.state.settings)
.then(() => {
this.setState({
alertMessage: t("Successfully saved settings"),
alertType: "success",
showAlert: true,
processing: false
});
})
.catch(error => {
let message = "";
if (error instanceof Error) {
message = error.message;
} else {
// Translate the API's error message
message = t("API Error: {{error}}", {
error: t(error.key, error.data)
});
}
this.setState({
alertMessage: message,
alertType: "danger",
showAlert: true,
processing: false
});
});
};
/**
@@ -101,37 +145,55 @@ class DHCPInfo extends Component {
* @param validator the validation function
*/
isSettingValid = (value, validator) => {
return (!this.state.active && value.length === 0) || validator(value);
return (
(!this.state.settings.active && value.length === 0) || validator(value)
);
};
hideAlert = () => {
this.setState({ showAlert: false });
};
render() {
const { t } = this.props;
const isIpStartValid = this.isSettingValid(
this.state.ip_start,
this.state.settings.ip_start,
isValidIpv4
);
const isIpEndValid = this.isSettingValid(
this.state.settings.ip_end,
isValidIpv4
);
const isIpEndValid = this.isSettingValid(this.state.ip_end, isValidIpv4);
const isRouterIpValid = this.isSettingValid(
this.state.router_ip,
this.state.settings.router_ip,
isValidIpv4
);
const isLeaseTimeValid = this.isSettingValid(
this.state.lease_time,
this.state.settings.lease_time,
isPositiveNumber
);
const isDomainValid = this.isSettingValid(
this.state.domain,
this.state.settings.domain,
isValidHostname
);
const alert = this.state.showAlert ? (
<Alert
message={this.state.alertMessage}
type={this.state.alertType}
onClick={this.hideAlert}
/>
) : null;
return (
<Form onSubmit={this.saveSettings}>
{alert}
<FormGroup check>
<Label check>
<Input
type="checkbox"
checked={this.state.active}
checked={this.state.settings.active}
onChange={this.onChange("active", "checked")}
/>
{t("Enabled")}
@@ -144,8 +206,8 @@ class DHCPInfo extends Component {
<Col sm={10}>
<Input
id="startIP"
disabled={!this.state.active}
value={this.state.ip_start}
disabled={!this.state.settings.active}
value={this.state.settings.ip_start}
onChange={this.onChange("ip_start", "value")}
invalid={!isIpStartValid}
/>
@@ -158,8 +220,8 @@ class DHCPInfo extends Component {
<Col sm={10}>
<Input
id="endIP"
disabled={!this.state.active}
value={this.state.ip_end}
disabled={!this.state.settings.active}
value={this.state.settings.ip_end}
onChange={this.onChange("ip_end", "value")}
invalid={!isIpEndValid}
/>
@@ -172,8 +234,8 @@ class DHCPInfo extends Component {
<Col sm={10}>
<Input
id="routerIP"
disabled={!this.state.active}
value={this.state.router_ip}
disabled={!this.state.settings.active}
value={this.state.settings.router_ip}
onChange={this.onChange("router_ip", "value")}
invalid={!isRouterIpValid}
/>
@@ -187,12 +249,12 @@ class DHCPInfo extends Component {
<InputGroup>
<Input
id="leaseTime"
disabled={!this.state.active}
value={this.state.lease_time}
disabled={!this.state.settings.active}
value={this.state.settings.lease_time}
onChange={this.onChange("lease_time", "value")}
invalid={!isLeaseTimeValid}
/>
<InputGroupAddon addonType={"append"}>Hours</InputGroupAddon>
<InputGroupAddon addonType="append">Hours</InputGroupAddon>
</InputGroup>
</Col>
</FormGroup>
@@ -203,8 +265,8 @@ class DHCPInfo extends Component {
<Col sm={10}>
<Input
id="domain"
disabled={!this.state.active}
value={this.state.domain}
disabled={!this.state.settings.active}
value={this.state.settings.domain}
onChange={this.onChange("domain", "value")}
invalid={!isDomainValid}
/>
@@ -214,8 +276,8 @@ class DHCPInfo extends Component {
<Label check>
<Input
type="checkbox"
disabled={!this.state.active}
checked={this.state.ipv6_support}
disabled={!this.state.settings.active}
checked={this.state.settings.ipv6_support}
onChange={this.onChange("ipv6_support", "checked")}
/>
{t("IPv6 Support")}
@@ -224,6 +286,7 @@ class DHCPInfo extends Component {
<Button
type="submit"
disabled={
this.state.processing ||
!isIpStartValid ||
!isIpEndValid ||
!isRouterIpValid ||
@@ -238,4 +301,4 @@ class DHCPInfo extends Component {
}
}
export default translate(["common", "settings"])(DHCPInfo);
export default translate(["common", "settings", "api-errors"])(DHCPInfo);
+135
View File
@@ -0,0 +1,135 @@
/* 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
* DHCPInfo 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 { shallow } from "enzyme";
import DHCPInfo from "./DHCPInfo";
import fetchMock from "fetch-mock";
const endpoint = "/admin/api/settings/dhcp";
const fakeData = {
active: true,
ip_start: "192.168.1.50",
ip_end: "192.168.1.150",
router_ip: "192.168.1.1",
lease_time: "24",
domain: "lan",
ipv6_support: false
};
it("retrieves settings correctly", async () => {
fetchMock.mock(endpoint, { body: fakeData });
const wrapper = shallow(<DHCPInfo />);
await tick();
expect(wrapper.state("settings")).toEqual(fakeData);
});
it("disables the apply button if an input is invalid", async () => {
fetchMock.mock(endpoint, { body: fakeData });
const wrapper = shallow(<DHCPInfo />);
await tick();
expect(wrapper.find("Button")).not.toBeDisabled();
wrapper.find("#domain").simulate("change", { target: { value: "" } });
expect(wrapper.find("Button")).toBeDisabled();
});
it("disables inputs only when DHCP is not enabled", async () => {
fetchMock.mock(endpoint, { body: fakeData });
const wrapper = shallow(<DHCPInfo />);
await tick();
wrapper.update();
expect(wrapper.find("#domain")).not.toBeDisabled();
wrapper.setState({
settings: { ...wrapper.state().settings, active: false }
});
expect(wrapper.find("#domain")).toBeDisabled();
});
it("disables the apply button when processing setting update", async () => {
fetchMock.get(endpoint, { body: fakeData });
fetchMock.put(endpoint, { body: { status: "success" } });
const wrapper = shallow(<DHCPInfo />);
await tick();
expect(wrapper.state().processing).toBeFalsy();
expect(wrapper.find("Button")).not.toBeDisabled();
wrapper.find("Form").simulate("submit", { preventDefault: jest.fn() });
expect(wrapper.state().processing).toBeTruthy();
expect(wrapper.find("Button")).toBeDisabled();
});
it("sends the correct data to the API when apply is clicked", async () => {
fetchMock.get(endpoint, { body: fakeData });
fetchMock.put(endpoint, { body: { status: "success" } });
const wrapper = shallow(<DHCPInfo />);
await tick();
expect(wrapper.state().processing).toBeFalsy();
expect(wrapper.find("Button")).not.toBeDisabled();
wrapper.find("Form").simulate("submit", { preventDefault: jest.fn() });
expect(wrapper.state().processing).toBeTruthy();
expect(wrapper.find("Button")).toBeDisabled();
// The output of lastCall is [url, request]
const updateRequest = fetchMock.lastCall(endpoint)[1];
expect(JSON.parse(updateRequest.body)).toEqual(fakeData);
});
it("shows a success message after successfully saving settings", async () => {
fetchMock.get(endpoint, { body: fakeData });
fetchMock.put(endpoint, { body: { status: "success" } });
const wrapper = shallow(<DHCPInfo />);
wrapper.find("Form").simulate("submit", { preventDefault: jest.fn() });
await tick();
wrapper.update();
expect(wrapper.find("Alert")).toHaveProp("type", "success");
});
it("shows an API error message if an API error occurs when saving settings", async () => {
fetchMock.get(endpoint, { body: fakeData });
fetchMock.put(endpoint, {
body: { error: { key: "unknown", message: "Unknown", data: null } }
});
const wrapper = shallow(<DHCPInfo />);
wrapper.find("Form").simulate("submit", { preventDefault: jest.fn() });
await tick();
wrapper.update();
expect(wrapper.find("Alert")).toHaveProp("type", "danger");
});
+2 -1
View File
@@ -20,7 +20,8 @@ export function setupI18n() {
"location",
"login",
"query-log",
"settings"
"settings",
"api-errors"
],
nsSeparator: false,
keySeparator: false,
+3
View File
@@ -81,5 +81,8 @@ export default {
},
getDHCPInfo() {
return http.get("settings/dhcp");
},
updateDHCPInfo(settings) {
return http.put("settings/dhcp", settings);
}
};
+4 -3
View File
@@ -54,7 +54,7 @@ function getTranslatedLanguages() {
.then(response => response.json())
.then(data => {
return data.result.languages
.filter(lang => lang.percentage >= 90)
.filter(lang => lang.percentage >= 70)
.map(lang => lang.code)
})
}
@@ -143,7 +143,8 @@ function fetchAllTags(lang) {
"lists",
"login",
"footer",
"settings"
"settings",
"api-errors"
];
const bar = multi.newBar(`${lang}\t:percent\t[:bar]`, {
@@ -166,7 +167,7 @@ fs.emptyDirSync(I18N_FOLDER);
console.log("Fetching languages");
getTranslatedLanguages()
.then(languages => {
console.log(`Languages over 90% translated: ${languages.join(", ")}`);
console.log(`Languages over 70% translated: ${languages.join(", ")}`);
for(const lang of languages)
fetchAllTags(lang);