Added select for listen interfaces

This commit is contained in:
Ildar Kamalov 2019-02-01 19:52:42 +03:00 committed by Eugene Bujak
parent 5abe5af707
commit f379d34813
10 changed files with 211 additions and 107 deletions

View File

@ -164,10 +164,11 @@
"install_settings_title": "Admin Web Interface", "install_settings_title": "Admin Web Interface",
"install_settings_listen": "Listen interface", "install_settings_listen": "Listen interface",
"install_settings_port": "Port", "install_settings_port": "Port",
"install_settings_interface_link": "Your AdGuard Home admin web interface is available on <0>{{link}}</0>", "install_settings_interface_link": "Your AdGuard Home admin web interface will be available on the following addresses: <0>{{link}}</0>",
"form_error_port": "Enter valid port value", "form_error_port": "Enter valid port value",
"install_settings_dns": "DNS server", "install_settings_dns": "DNS server",
"install_settings_dns_desc": "You will need to configure your devices or router to use the DNS server at <0>{{ip}}</0>", "install_settings_dns_desc": "You will need to configure your devices or router to use the DNS server at <0>{{ip}}</0>",
"install_settings_all_interfaces": "All interfaces",
"install_auth_title": "Authentication", "install_auth_title": "Authentication",
"install_auth_desc": "It is highly recommended to configure password authentication to your AdGuard Home admin web interface. Even if it is accessible only in your local network, it is still important to have it protected from unrestricted access.", "install_auth_desc": "It is highly recommended to configure password authentication to your AdGuard Home admin web interface. Even if it is accessible only in your local network, it is still important to have it protected from unrestricted access.",
"install_auth_username": "Username", "install_auth_username": "Username",
@ -182,6 +183,7 @@
"install_submit_desc": "The setup procedure is finished and you are ready to start using AdGuard Home.", "install_submit_desc": "The setup procedure is finished and you are ready to start using AdGuard Home.",
"install_devices_router": "Router", "install_devices_router": "Router",
"install_devices_router_desc": "This setup will automatically cover all the devices connected to your home router and you will not need to configure each of them manually.", "install_devices_router_desc": "This setup will automatically cover all the devices connected to your home router and you will not need to configure each of them manually.",
"install_devices_address": "AdGuard Home DNS server is listening to the following addresses",
"install_devices_router_list_1": "Open the preferences for your router. Usually, you can access it from your browser via a URL (like http://192.168.0.1/ or http://192.168.1.1/). You may be asked to enter the password. If you don't remember it, you can often reset the password by pressing a button on the router itself. Some routers require a specific application, which in that case should be already installed on your computer/phone.", "install_devices_router_list_1": "Open the preferences for your router. Usually, you can access it from your browser via a URL (like http://192.168.0.1/ or http://192.168.1.1/). You may be asked to enter the password. If you don't remember it, you can often reset the password by pressing a button on the router itself. Some routers require a specific application, which in that case should be already installed on your computer/phone.",
"install_devices_router_list_2": "Find the DHCP/DNS settings. Look for the DNS letters next to a field which allows two or three sets of numbers, each broken into four groups of one to three digits.", "install_devices_router_list_2": "Find the DHCP/DNS settings. Look for the DNS letters next to a field which allows two or three sets of numbers, each broken into four groups of one to three digits.",
"install_devices_router_list_3": "Enter your AdGuard Home server addresses there.", "install_devices_router_list_3": "Enter your AdGuard Home server addresses there.",

View File

@ -48,8 +48,10 @@ export const setAllSettings = values => async (dispatch) => {
await apiClient.setAllSettings(config); await apiClient.setAllSettings(config);
dispatch(setAllSettingsSuccess()); dispatch(setAllSettingsSuccess());
dispatch(addSuccessToast('install_saved')); dispatch(addSuccessToast('install_saved'));
dispatch(nextStep());
} catch (error) { } catch (error) {
dispatch(addErrorToast({ error })); dispatch(addErrorToast({ error }));
dispatch(setAllSettingsFailure()); dispatch(setAllSettingsFailure());
dispatch(prevStep());
} }
}; };

View File

@ -338,16 +338,16 @@ export default class Api {
} }
// Installation // Installation
GET_DEFAULT_ADDRESSES = { path: 'install/get_default_addresses', method: 'GET' }; INSTALL_GET_ADDRESSES = { path: 'install/get_addresses', method: 'GET' };
SET_ALL_SETTINGS = { path: 'install/set_all_settings', method: 'POST' }; INSTALL_CONFIGURE = { path: 'install/configure', method: 'POST' };
getDefaultAddresses() { getDefaultAddresses() {
const { path, method } = this.GET_DEFAULT_ADDRESSES; const { path, method } = this.INSTALL_GET_ADDRESSES;
return this.makeRequest(path, method); return this.makeRequest(path, method);
} }
setAllSettings(config) { setAllSettings(config) {
const { path, method } = this.SET_ALL_SETTINGS; const { path, method } = this.INSTALL_CONFIGURE;
const parameters = { const parameters = {
data: config, data: config,
headers: { 'Content-Type': 'application/json' }, headers: { 'Content-Type': 'application/json' },

View File

@ -19,7 +19,26 @@ class Controls extends Component {
} }
} }
renderButtons(step) { renderPrevButton(step) {
switch (step) {
case 2:
case 3:
case 4:
return (
<button
type="button"
className="btn btn-secondary btn-standard btn-lg"
onClick={this.props.prevStep}
>
<Trans>back</Trans>
</button>
);
default:
return false;
}
}
renderNextButton(step) {
switch (step) { switch (step) {
case 1: case 1:
return ( return (
@ -34,48 +53,30 @@ class Controls extends Component {
case 2: case 2:
case 3: case 3:
return ( return (
<div className="btn-list"> <button
<button type="submit"
type="button" className="btn btn-success btn-standard btn-lg"
className="btn btn-secondary btn-standard btn-lg" disabled={this.props.invalid || this.props.pristine}
onClick={this.props.prevStep} >
> <Trans>next</Trans>
<Trans>back</Trans> </button>
</button>
<button
type="submit"
className="btn btn-success btn-standard btn-lg"
disabled={this.props.invalid || this.props.pristine}
>
<Trans>next</Trans>
</button>
</div>
); );
case 4: case 4:
return ( return (
<div className="btn-list"> <button
<button type="button"
type="button" className="btn btn-success btn-standard btn-lg"
className="btn btn-secondary btn-standard btn-lg" onClick={this.props.nextStep}
onClick={this.props.prevStep} >
> <Trans>next</Trans>
<Trans>back</Trans> </button>
</button>
<button
type="button"
className="btn btn-success btn-standard btn-lg"
onClick={this.props.nextStep}
>
<Trans>next</Trans>
</button>
</div>
); );
case 5: case 5:
return ( return (
<button <button
type="submit" type="button"
className="btn btn-success btn-standard btn-lg" className="btn btn-success btn-standard btn-lg"
disabled={this.props.submitting || this.props.pristine} onClick={this.props.openDashboard}
> >
<Trans>open_dashboard</Trans> <Trans>open_dashboard</Trans>
</button> </button>
@ -88,7 +89,10 @@ class Controls extends Component {
render() { render() {
return ( return (
<div className="setup__nav"> <div className="setup__nav">
{this.renderButtons(this.props.step)} <div className="btn-list">
{this.renderPrevButton(this.props.step)}
{this.renderNextButton(this.props.step)}
</div>
</div> </div>
); );
} }
@ -98,9 +102,10 @@ Controls.propTypes = {
step: PropTypes.number.isRequired, step: PropTypes.number.isRequired,
nextStep: PropTypes.func, nextStep: PropTypes.func,
prevStep: PropTypes.func, prevStep: PropTypes.func,
pristine: PropTypes.bool, openDashboard: PropTypes.func,
submitting: PropTypes.bool, submitting: PropTypes.bool,
invalid: PropTypes.bool, invalid: PropTypes.bool,
pristine: PropTypes.bool,
}; };
const mapStateToProps = (state) => { const mapStateToProps = (state) => {

View File

@ -1,19 +1,29 @@
import React from 'react'; import React from 'react';
import PropTypes from 'prop-types';
import { connect } from 'react-redux';
import { reduxForm, formValueSelector } from 'redux-form';
import { Trans, withNamespaces } from 'react-i18next'; import { Trans, withNamespaces } from 'react-i18next';
import flow from 'lodash/flow';
import Tabs from '../../components/ui/Tabs'; import Tabs from '../../components/ui/Tabs';
import Icons from '../../components/ui/Icons'; import Icons from '../../components/ui/Icons';
import Controls from './Controls'; import Controls from './Controls';
const Devices = () => ( let Devices = props => (
<div className="setup__step"> <div className="setup__step">
<div className="setup__group"> <div className="setup__group">
<div className="setup__subtitle"> <div className="setup__subtitle">
<Trans>install_devices_title</Trans> <Trans>install_devices_title</Trans>
</div> </div>
<p className="setup__desc"> <div className="setup__desc">
<Trans>install_devices_desc</Trans> <Trans>install_devices_desc</Trans>
</p> <div className="mt-1">
<Trans>install_devices_address</Trans>:
</div>
<div>
<strong>{`${props.dnsIp}:${props.dnsPort}`}</strong>
</div>
</div>
<Icons /> <Icons />
<Tabs> <Tabs>
<div label="Router"> <div label="Router">
@ -90,4 +100,28 @@ const Devices = () => (
</div> </div>
); );
export default withNamespaces()(Devices); Devices.propTypes = {
dnsIp: PropTypes.string.isRequired,
dnsPort: PropTypes.number.isRequired,
};
const selector = formValueSelector('install');
Devices = connect((state) => {
const dnsIp = selector(state, 'dns.ip');
const dnsPort = selector(state, 'dns.port');
return {
dnsIp,
dnsPort,
};
})(Devices);
export default flow([
withNamespaces(),
reduxForm({
form: 'install',
destroyOnUnmount: false,
forceUnregisterOnUnmount: true,
}),
])(Devices);

View File

@ -7,7 +7,6 @@ import flow from 'lodash/flow';
import Controls from './Controls'; import Controls from './Controls';
import renderField from './renderField'; import renderField from './renderField';
import { R_IPV4 } from '../../helpers/constants';
const required = (value) => { const required = (value) => {
if (value || value === 0) { if (value || value === 0) {
@ -16,13 +15,6 @@ const required = (value) => {
return <Trans>form_error_required</Trans>; return <Trans>form_error_required</Trans>;
}; };
const ipv4 = (value) => {
if (value && !new RegExp(R_IPV4).test(value)) {
return <Trans>form_error_ip_format</Trans>;
}
return false;
};
const port = (value) => { const port = (value) => {
if (value < 1 || value > 65535) { if (value < 1 || value > 65535) {
return <Trans>form_error_port</Trans>; return <Trans>form_error_port</Trans>;
@ -32,6 +24,29 @@ const port = (value) => {
const toNumber = value => value && parseInt(value, 10); const toNumber = value => value && parseInt(value, 10);
const renderInterfaces = (interfaces => (
Object.keys(interfaces).map((item) => {
const option = interfaces[item];
const { name } = option;
const onlyIPv6 = option.ip_addresses.every(ip => ip.includes(':'));
let interfaceIP = option.ip_addresses[0];
if (!onlyIPv6) {
option.ip_addresses.forEach((ip) => {
if (!ip.includes(':')) {
interfaceIP = ip;
}
});
}
return (
<option value={interfaceIP} key={name}>
{name} - {interfaceIP}
</option>
);
})
));
let Settings = (props) => { let Settings = (props) => {
const { const {
handleSubmit, handleSubmit,
@ -39,7 +54,10 @@ let Settings = (props) => {
interfacePort, interfacePort,
dnsIp, dnsIp,
dnsPort, dnsPort,
interfaces,
invalid, invalid,
webWarning,
dnsWarning,
} = props; } = props;
const dnsAddress = dnsPort && dnsPort !== 53 ? `${dnsIp}:${dnsPort}` : dnsIp; const dnsAddress = dnsPort && dnsPort !== 53 ? `${dnsIp}:${dnsPort}` : dnsIp;
const interfaceAddress = interfacePort ? `http://${interfaceIp}:${interfacePort}` : `http://${interfaceIp}`; const interfaceAddress = interfacePort ? `http://${interfaceIp}:${interfacePort}` : `http://${interfaceIp}`;
@ -58,12 +76,14 @@ let Settings = (props) => {
</label> </label>
<Field <Field
name="web.ip" name="web.ip"
component={renderField} component="select"
type="text" className="form-control custom-select"
className="form-control" >
placeholder="0.0.0.0" <option value="0.0.0.0">
validate={[ipv4, required]} <Trans>install_settings_all_interfaces</Trans>
/> </option>
{renderInterfaces(interfaces)}
</Field>
</div> </div>
</div> </div>
<div className="col-4"> <div className="col-4">
@ -90,6 +110,11 @@ let Settings = (props) => {
> >
install_settings_interface_link install_settings_interface_link
</Trans> </Trans>
{webWarning &&
<div className="text-danger mt-2">
{webWarning}
</div>
}
</div> </div>
</div> </div>
<div className="setup__group"> <div className="setup__group">
@ -104,12 +129,14 @@ let Settings = (props) => {
</label> </label>
<Field <Field
name="dns.ip" name="dns.ip"
component={renderField} component="select"
type="text" className="form-control custom-select"
className="form-control" >
placeholder="0.0.0.0" <option value="0.0.0.0" defaultValue>
validate={[ipv4, required]} <Trans>install_settings_all_interfaces</Trans>
/> </option>
{renderInterfaces(interfaces)}
</Field>
</div> </div>
</div> </div>
<div className="col-4"> <div className="col-4">
@ -129,14 +156,19 @@ let Settings = (props) => {
</div> </div>
</div> </div>
</div> </div>
<p className="setup__desc"> <div className="setup__desc">
<Trans <Trans
components={[<strong key="0">ip</strong>]} components={[<strong key="0">ip</strong>]}
values={{ ip: dnsAddress }} values={{ ip: dnsAddress }}
> >
install_settings_dns_desc install_settings_dns_desc
</Trans> </Trans>
</p> {dnsWarning &&
<div className="text-danger mt-2">
{dnsWarning}
</div>
}
</div>
</div> </div>
<Controls invalid={invalid} /> <Controls invalid={invalid} />
</form> </form>
@ -155,17 +187,13 @@ Settings.propTypes = {
PropTypes.string, PropTypes.string,
PropTypes.number, PropTypes.number,
]), ]),
webWarning: PropTypes.string.isRequired,
dnsWarning: PropTypes.string.isRequired,
interfaces: PropTypes.object.isRequired,
invalid: PropTypes.bool.isRequired, invalid: PropTypes.bool.isRequired,
initialValues: PropTypes.object, initialValues: PropTypes.object,
}; };
Settings.defaultProps = {
interfaceIp: '192.168.0.1',
interfacePort: 3000,
dnsIp: '192.168.0.1',
dnsPort: 53,
};
const selector = formValueSelector('install'); const selector = formValueSelector('install');
Settings = connect((state) => { Settings = connect((state) => {

View File

@ -54,6 +54,7 @@
} }
.setup__desc { .setup__desc {
margin-bottom: 20px;
font-size: 15px; font-size: 15px;
} }

View File

@ -1,43 +1,53 @@
import React, { Component } from 'react'; import React from 'react';
import { connect } from 'react-redux';
import PropTypes from 'prop-types'; import PropTypes from 'prop-types';
import { reduxForm } from 'redux-form'; import { reduxForm, formValueSelector } from 'redux-form';
import { Trans, withNamespaces } from 'react-i18next'; import { Trans, withNamespaces } from 'react-i18next';
import flow from 'lodash/flow'; import flow from 'lodash/flow';
import Controls from './Controls'; import Controls from './Controls';
class Submit extends Component { let Submit = props => (
render() { <div className="setup__step">
const { <div className="setup__group">
handleSubmit, <h1 className="setup__title">
pristine, <Trans>install_submit_title</Trans>
submitting, </h1>
} = this.props; <p className="setup__desc">
<Trans>install_submit_desc</Trans>
return ( </p>
<div className="setup__step"> </div>
<div className="setup__group"> <form onSubmit={props.handleSubmit}>
<h1 className="setup__title"> <Controls
<Trans>install_submit_title</Trans> submitting={props.submitting}
</h1> pristine={props.pristine}
<p className="setup__desc"> address={`http://${props.interfaceIp}`}
<Trans>install_submit_desc</Trans> />
</p> </form>
</div> </div>
<form onSubmit={handleSubmit}> );
<Controls submitting={submitting} pristine={pristine} />
</form>
</div>
);
}
}
Submit.propTypes = { Submit.propTypes = {
interfaceIp: PropTypes.string.isRequired,
interfacePort: PropTypes.number.isRequired,
handleSubmit: PropTypes.func.isRequired, handleSubmit: PropTypes.func.isRequired,
pristine: PropTypes.bool.isRequired, pristine: PropTypes.bool.isRequired,
submitting: PropTypes.bool.isRequired, submitting: PropTypes.bool.isRequired,
}; };
const selector = formValueSelector('install');
Submit = connect((state) => {
const interfaceIp = selector(state, 'web.ip');
const interfacePort = selector(state, 'web.port');
return {
interfaceIp,
interfacePort,
};
})(Submit);
export default flow([ export default flow([
withNamespaces(), withNamespaces(),
reduxForm({ reduxForm({

View File

@ -29,6 +29,10 @@ class Setup extends Component {
this.props.setAllSettings(values); this.props.setAllSettings(values);
}; };
openDashboard = () => {
console.log('Open dashboard');
}
nextStep = () => { nextStep = () => {
if (this.props.install.step < INSTALL_TOTAL_STEPS) { if (this.props.install.step < INSTALL_TOTAL_STEPS) {
this.props.nextStep(); this.props.nextStep();
@ -41,7 +45,7 @@ class Setup extends Component {
} }
} }
renderPage(step, config) { renderPage(step, config, interfaces) {
switch (step) { switch (step) {
case 1: case 1:
return <Greeting />; return <Greeting />;
@ -49,17 +53,20 @@ class Setup extends Component {
return ( return (
<Settings <Settings
initialValues={config} initialValues={config}
interfaces={interfaces}
webWarning={config.web.warning}
dnsWarning={config.dns.warning}
onSubmit={this.nextStep} onSubmit={this.nextStep}
/> />
); );
case 3: case 3:
return ( return (
<Auth onSubmit={this.nextStep} /> <Auth onSubmit={this.handleFormSubmit} />
); );
case 4: case 4:
return <Devices />; return <Devices />;
case 5: case 5:
return <Submit onSubmit={this.handleFormSubmit} />; return <Submit onSubmit={this.openDashboard} />;
default: default:
return false; return false;
} }
@ -71,6 +78,7 @@ class Setup extends Component {
step, step,
web, web,
dns, dns,
interfaces,
} = this.props.install; } = this.props.install;
return ( return (
@ -81,7 +89,7 @@ class Setup extends Component {
<div className="setup"> <div className="setup">
<div className="setup__container"> <div className="setup__container">
<img src={logo} className="setup__logo" alt="logo" /> <img src={logo} className="setup__logo" alt="logo" />
{this.renderPage(step, { web, dns })} {this.renderPage(step, { web, dns }, interfaces)}
<Progress step={step} /> <Progress step={step} />
</div> </div>
</div> </div>

View File

@ -10,7 +10,10 @@ const install = handleActions({
[actions.getDefaultAddressesRequest]: state => ({ ...state, processingDefault: true }), [actions.getDefaultAddressesRequest]: state => ({ ...state, processingDefault: true }),
[actions.getDefaultAddressesFailure]: state => ({ ...state, processingDefault: false }), [actions.getDefaultAddressesFailure]: state => ({ ...state, processingDefault: false }),
[actions.getDefaultAddressesSuccess]: (state, { payload }) => { [actions.getDefaultAddressesSuccess]: (state, { payload }) => {
const newState = { ...state, ...payload, processingDefault: false }; const values = payload;
values.web.ip = state.web.ip;
values.dns.ip = state.dns.ip;
const newState = { ...state, ...values, processingDefault: false };
return newState; return newState;
}, },
@ -23,6 +26,17 @@ const install = handleActions({
}, { }, {
step: INSTALL_FIRST_STEP, step: INSTALL_FIRST_STEP,
processingDefault: true, processingDefault: true,
web: {
ip: '0.0.0.0',
port: 80,
warning: '',
},
dns: {
ip: '0.0.0.0',
port: 53,
warning: '',
},
interfaces: {},
}); });
const toasts = handleActions({ const toasts = handleActions({