mirror of
https://github.com/gosticks/web.git
synced 2026-10-06 22:57:01 +00:00
Merge branch 'development' into feature/with-api-component
This commit is contained in:
@@ -0,0 +1 @@
|
||||
{}
|
||||
@@ -0,0 +1 @@
|
||||
{}
|
||||
@@ -0,0 +1 @@
|
||||
{}
|
||||
@@ -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}}"
|
||||
}
|
||||
@@ -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"
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
{}
|
||||
@@ -0,0 +1 @@
|
||||
{}
|
||||
@@ -0,0 +1 @@
|
||||
{}
|
||||
@@ -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;
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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
@@ -20,7 +20,8 @@ export function setupI18n() {
|
||||
"location",
|
||||
"login",
|
||||
"query-log",
|
||||
"settings"
|
||||
"settings",
|
||||
"api-errors"
|
||||
],
|
||||
nsSeparator: false,
|
||||
keySeparator: false,
|
||||
|
||||
@@ -81,5 +81,8 @@ export default {
|
||||
},
|
||||
getDHCPInfo() {
|
||||
return http.get("settings/dhcp");
|
||||
},
|
||||
updateDHCPInfo(settings) {
|
||||
return http.put("settings/dhcp", settings);
|
||||
}
|
||||
};
|
||||
|
||||
@@ -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);
|
||||
|
||||
Reference in New Issue
Block a user