diff --git a/client/src/__locales/en.json b/client/src/__locales/en.json index ac8560c3..171df718 100644 --- a/client/src/__locales/en.json +++ b/client/src/__locales/en.json @@ -257,5 +257,7 @@ "reset_settings": "Reset settings", "update_announcement": "AdGuard Home {{version}} is now available! <0>Click here<\/0> for more info.", "setup_guide": "Setup guide", - "dns_addresses": "DNS addresses" + "dns_addresses": "DNS addresses", + "down": "Down", + "fix": "Fix" } \ No newline at end of file diff --git a/client/src/actions/install.js b/client/src/actions/install.js index c738f75d..3070ff2d 100644 --- a/client/src/actions/install.js +++ b/client/src/actions/install.js @@ -44,3 +44,18 @@ export const setAllSettings = values => async (dispatch) => { dispatch(prevStep()); } }; + +export const checkConfigRequest = createAction('CHECK_CONFIG_REQUEST'); +export const checkConfigFailure = createAction('CHECK_CONFIG_FAILURE'); +export const checkConfigSuccess = createAction('CHECK_CONFIG_SUCCESS'); + +export const checkConfig = values => async (dispatch) => { + dispatch(checkConfigRequest()); + try { + const check = await apiClient.checkConfig(values); + dispatch(checkConfigSuccess(check)); + } catch (error) { + dispatch(addErrorToast({ error })); + dispatch(checkConfigFailure()); + } +}; diff --git a/client/src/api/Api.js b/client/src/api/Api.js index a0ca7ed2..6d8a2f52 100644 --- a/client/src/api/Api.js +++ b/client/src/api/Api.js @@ -350,6 +350,7 @@ export default class Api { // Installation INSTALL_GET_ADDRESSES = { path: 'install/get_addresses', method: 'GET' }; INSTALL_CONFIGURE = { path: 'install/configure', method: 'POST' }; + INSTALL_CHECK_CONFIG = { path: 'install/check_config', method: 'POST' }; getDefaultAddresses() { const { path, method } = this.INSTALL_GET_ADDRESSES; @@ -365,6 +366,15 @@ export default class Api { return this.makeRequest(path, method, parameters); } + checkConfig(config) { + const { path, method } = this.INSTALL_CHECK_CONFIG; + const parameters = { + data: config, + headers: { 'Content-Type': 'application/json' }, + }; + return this.makeRequest(path, method, parameters); + } + // DNS-over-HTTPS and DNS-over-TLS TLS_STATUS = { path: 'tls/status', method: 'GET' }; TLS_CONFIG = { path: 'tls/configure', method: 'POST' }; diff --git a/client/src/install/Setup/Controls.js b/client/src/install/Setup/Controls.js index 5560c511..489ce08d 100644 --- a/client/src/install/Setup/Controls.js +++ b/client/src/install/Setup/Controls.js @@ -55,6 +55,8 @@ class Controls extends Component { invalid || pristine || install.processingSubmit + || install.dns.status + || install.web.status } > next diff --git a/client/src/install/Setup/Settings.js b/client/src/install/Setup/Settings.js index 25fd2d76..ff11f0ab 100644 --- a/client/src/install/Setup/Settings.js +++ b/client/src/install/Setup/Settings.js @@ -1,4 +1,4 @@ -import React from 'react'; +import React, { Fragment } from 'react'; import { connect } from 'react-redux'; import PropTypes from 'prop-types'; import { Field, reduxForm, formValueSelector } from 'redux-form'; @@ -30,10 +30,25 @@ const toNumber = value => value && parseInt(value, 10); const renderInterfaces = (interfaces => ( Object.keys(interfaces).map((item) => { const option = interfaces[item]; - const { name } = option; + const { + name, + ip_addresses, + flags, + } = option; - if (option.ip_addresses && option.ip_addresses.length > 0) { + if (option && ip_addresses && ip_addresses.length > 0) { const ip = getInterfaceIp(option); + const isDown = flags && flags.includes('down'); + + if (isDown) { + return ( + + + {name} - {ip} (down) + + + ); + } return ( @@ -49,15 +64,24 @@ const renderInterfaces = (interfaces => ( let Settings = (props) => { const { handleSubmit, + handleChange, + handleAutofix, webIp, webPort, dnsIp, dnsPort, interfaces, invalid, - webWarning, - dnsWarning, + config, } = props; + const { + status: webStatus, + can_autofix: isWebFixAvailable, + } = config.web; + const { + status: dnsStatus, + can_autofix: isDnsFixAvailable, + } = config.dns; return ( @@ -75,6 +99,7 @@ let Settings = (props) => { name="web.ip" component="select" className="form-control custom-select" + onChange={handleChange} > install_settings_all_interfaces @@ -96,9 +121,26 @@ let Settings = (props) => { placeholder="80" validate={[port, required]} normalize={toNumber} + onChange={handleChange} /> + + {webStatus && + + {webStatus} + {isWebFixAvailable && + handleAutofix('web', webIp, webPort)} + > + fix + + } + + } + install_settings_interface_link @@ -109,11 +151,6 @@ let Settings = (props) => { port={webPort} /> - {webWarning && - - {webWarning} - - } @@ -130,6 +167,7 @@ let Settings = (props) => { name="dns.ip" component="select" className="form-control custom-select" + onChange={handleChange} > install_settings_all_interfaces @@ -151,9 +189,26 @@ let Settings = (props) => { placeholder="80" validate={[port, required]} normalize={toNumber} + onChange={handleChange} /> + + {dnsStatus && + + {dnsStatus} + {isDnsFixAvailable && + handleAutofix('dns', dnsIp, dnsPort)} + > + fix + + } + + } + install_settings_dns_desc @@ -165,11 +220,6 @@ let Settings = (props) => { isDns={true} /> - {dnsWarning && - - {dnsWarning} - - } @@ -179,8 +229,11 @@ let Settings = (props) => { Settings.propTypes = { handleSubmit: PropTypes.func.isRequired, + handleChange: PropTypes.func, + handleAutofix: PropTypes.func, webIp: PropTypes.string.isRequired, dnsIp: PropTypes.string.isRequired, + config: PropTypes.object.isRequired, webPort: PropTypes.oneOfType([ PropTypes.string, PropTypes.number, @@ -189,8 +242,6 @@ Settings.propTypes = { PropTypes.string, PropTypes.number, ]), - webWarning: PropTypes.string.isRequired, - dnsWarning: PropTypes.string.isRequired, interfaces: PropTypes.object.isRequired, invalid: PropTypes.bool.isRequired, initialValues: PropTypes.object, diff --git a/client/src/install/Setup/Setup.css b/client/src/install/Setup/Setup.css index cf58bc3c..b71c5f55 100644 --- a/client/src/install/Setup/Setup.css +++ b/client/src/install/Setup/Setup.css @@ -115,3 +115,7 @@ padding-left: 30px; padding-right: 30px; } + +.setup__error { + margin: -5px 0 5px; +} diff --git a/client/src/install/Setup/index.js b/client/src/install/Setup/index.js index ed901e46..c1ae6a43 100644 --- a/client/src/install/Setup/index.js +++ b/client/src/install/Setup/index.js @@ -1,6 +1,7 @@ import React, { Component, Fragment } from 'react'; import { connect } from 'react-redux'; import PropTypes from 'prop-types'; +import debounce from 'lodash/debounce'; import * as actionCreators from '../../actions/install'; import { getWebAddress } from '../../helpers/helpers'; @@ -8,6 +9,7 @@ import { INSTALL_FIRST_STEP, INSTALL_TOTAL_STEPS, ALL_INTERFACES_IP, + DEBOUNCE_TIMEOUT, } from '../../helpers/constants'; import Loading from '../../components/ui/Loading'; @@ -34,6 +36,30 @@ class Setup extends Component { this.props.setAllSettings(values); }; + handleFormChange = debounce((values) => { + if (values && values.web.port && values.dns.port) { + this.props.checkConfig(values); + } + }, DEBOUNCE_TIMEOUT); + + handleAutofix = (type, ip, port) => { + const data = { + ip, + port, + autofix: true, + }; + + if (type === 'web') { + this.props.checkConfig({ + web: { ...data }, + }); + } else { + this.props.checkConfig({ + dns: { ...data }, + }); + } + }; + openDashboard = (ip, port) => { let address = getWebAddress(ip, port); @@ -63,11 +89,12 @@ class Setup extends Component { case 2: return ( ); case 3: @@ -116,6 +143,7 @@ class Setup extends Component { Setup.propTypes = { getDefaultAddresses: PropTypes.func.isRequired, setAllSettings: PropTypes.func.isRequired, + checkConfig: PropTypes.func.isRequired, nextStep: PropTypes.func.isRequired, prevStep: PropTypes.func.isRequired, install: PropTypes.object.isRequired, diff --git a/client/src/reducers/install.js b/client/src/reducers/install.js index 7533f152..f1fb6f28 100644 --- a/client/src/reducers/install.js +++ b/client/src/reducers/install.js @@ -10,10 +10,13 @@ const install = handleActions({ [actions.getDefaultAddressesRequest]: state => ({ ...state, processingDefault: true }), [actions.getDefaultAddressesFailure]: state => ({ ...state, processingDefault: false }), [actions.getDefaultAddressesSuccess]: (state, { payload }) => { - const values = payload; - values.web.ip = state.web.ip; - values.dns.ip = state.dns.ip; - const newState = { ...state, ...values, processingDefault: false }; + const { interfaces } = payload; + const web = { ...state.web, ...payload.web }; + const dns = { ...state.dns, ...payload.dns }; + + const newState = { + ...state, web, dns, interfaces, processingDefault: false, + }; return newState; }, @@ -23,19 +26,34 @@ const install = handleActions({ [actions.setAllSettingsRequest]: state => ({ ...state, processingSubmit: true }), [actions.setAllSettingsFailure]: state => ({ ...state, processingSubmit: false }), [actions.setAllSettingsSuccess]: state => ({ ...state, processingSubmit: false }), + + [actions.checkConfigRequest]: state => ({ ...state, processingCheck: true }), + [actions.checkConfigFailure]: state => ({ ...state, processingCheck: false }), + [actions.checkConfigSuccess]: (state, { payload }) => { + const web = { ...state.web, ...payload.web }; + const dns = { ...state.dns, ...payload.dns }; + + const newState = { + ...state, web, dns, processingCheck: false, + }; + return newState; + }, }, { step: INSTALL_FIRST_STEP, processingDefault: true, processingSubmit: false, + processingCheck: false, web: { ip: '0.0.0.0', port: 80, - warning: '', + status: '', + can_autofix: false, }, dns: { ip: '0.0.0.0', port: 53, - warning: '', + status: '', + can_autofix: false, }, interfaces: {}, });