* client: upstream form

This commit is contained in:
Ildar Kamalov 2019-03-06 14:45:21 +03:00
parent e973c4b174
commit 5d6c980ac7
12 changed files with 256 additions and 129 deletions

View file

@ -246,5 +246,8 @@
"form_error_equal": "Shouldn't be equal",
"form_error_password": "Password mismatched",
"reset_settings": "Reset settings",
"update_announcement": "AdGuard Home {{version}} is now available! <0>Click here</0> for more info."
"update_announcement": "AdGuard Home {{version}} is now available! <0>Click here</0> for more info.",
"upstream_parallel": "Use parallel queries to speed up resolving by simultaneously querying all upstream servers",
"bootstrap_dns": "Bootstrap DNS",
"bootstrap_dns_desc": "Bootstrap DNS for DNS-over-HTTPS and DNS-over-TLS servers"
}

View file

@ -3,7 +3,7 @@ import round from 'lodash/round';
import { t } from 'i18next';
import { showLoading, hideLoading } from 'react-redux-loading-bar';
import { normalizeHistory, normalizeFilteringStatus, normalizeLogs } from '../helpers/helpers';
import { normalizeHistory, normalizeFilteringStatus, normalizeLogs, normalizeTextarea } from '../helpers/helpers';
import { SETTINGS_NAMES } from '../helpers/constants';
import Api from '../api/Api';
@ -452,10 +452,14 @@ export const setUpstreamRequest = createAction('SET_UPSTREAM_REQUEST');
export const setUpstreamFailure = createAction('SET_UPSTREAM_FAILURE');
export const setUpstreamSuccess = createAction('SET_UPSTREAM_SUCCESS');
export const setUpstream = url => async (dispatch) => {
export const setUpstream = config => async (dispatch) => {
dispatch(setUpstreamRequest());
try {
await apiClient.setUpstream(url);
const values = { ...config };
values.bootstrap_dns = (values.bootstrap_dns && normalizeTextarea(values.bootstrap_dns)) || '';
values.upstream_dns = (values.upstream_dns && normalizeTextarea(values.upstream_dns)) || '';
await apiClient.setUpstream(values);
dispatch(addSuccessToast('updated_upstream_dns_toast'));
dispatch(setUpstreamSuccess());
} catch (error) {
@ -468,9 +472,10 @@ export const testUpstreamRequest = createAction('TEST_UPSTREAM_REQUEST');
export const testUpstreamFailure = createAction('TEST_UPSTREAM_FAILURE');
export const testUpstreamSuccess = createAction('TEST_UPSTREAM_SUCCESS');
export const testUpstream = servers => async (dispatch) => {
export const testUpstream = values => async (dispatch) => {
dispatch(testUpstreamRequest());
try {
const servers = normalizeTextarea(values);
const upstreamResponse = await apiClient.testUpstream(servers);
const testMessages = Object.keys(upstreamResponse).map((key) => {

View file

@ -34,7 +34,7 @@ export default class Api {
GLOBAL_QUERY_LOG = { path: 'querylog', method: 'GET' };
GLOBAL_QUERY_LOG_ENABLE = { path: 'querylog_enable', method: 'POST' };
GLOBAL_QUERY_LOG_DISABLE = { path: 'querylog_disable', method: 'POST' };
GLOBAL_SET_UPSTREAM_DNS = { path: 'set_upstream_dns', method: 'POST' };
GLOBAL_SET_UPSTREAM_DNS = { path: 'set_upstreams_config', method: 'POST' };
GLOBAL_TEST_UPSTREAM_DNS = { path: 'test_upstream_dns', method: 'POST' };
GLOBAL_VERSION = { path: 'version.json', method: 'GET' };
GLOBAL_ENABLE_PROTECTION = { path: 'enable_protection', method: 'POST' };
@ -110,7 +110,7 @@ export default class Api {
const { path, method } = this.GLOBAL_SET_UPSTREAM_DNS;
const config = {
data: url,
header: { 'Content-Type': 'text/plain' },
headers: { 'Content-Type': 'application/json' },
};
return this.makeRequest(path, method, config);
}
@ -119,7 +119,7 @@ export default class Api {
const { path, method } = this.GLOBAL_TEST_UPSTREAM_DNS;
const config = {
data: servers,
header: { 'Content-Type': 'text/plain' },
headers: { 'Content-Type': 'application/json' },
};
return this.makeRequest(path, method, config);
}

View file

@ -1,97 +0,0 @@
import React, { Component } from 'react';
import PropTypes from 'prop-types';
import classnames from 'classnames';
import { Trans, withNamespaces } from 'react-i18next';
import Card from '../ui/Card';
class Upstream extends Component {
handleChange = (e) => {
const { value } = e.currentTarget;
this.props.handleUpstreamChange(value);
};
handleSubmit = (e) => {
e.preventDefault();
this.props.handleUpstreamSubmit();
};
handleTest = () => {
this.props.handleUpstreamTest();
}
render() {
const testButtonClass = classnames({
'btn btn-primary btn-standard mr-2': true,
'btn btn-primary btn-standard mr-2 btn-loading': this.props.processingTestUpstream,
});
const { t } = this.props;
return (
<Card
title={ t('upstream_dns') }
subtitle={ t('upstream_dns_hint') }
bodyType="card-body box-body--settings"
>
<div className="row">
<div className="col">
<form>
<textarea
className="form-control form-control--textarea"
value={this.props.upstreamDns}
onChange={this.handleChange}
/>
<div className="card-actions">
<button
className={testButtonClass}
type="button"
onClick={this.handleTest}
>
<Trans>test_upstream_btn</Trans>
</button>
<button
className="btn btn-success btn-standard"
type="submit"
onClick={this.handleSubmit}
>
<Trans>apply_btn</Trans>
</button>
</div>
</form>
<hr/>
<div className="list leading-loose">
<Trans>examples_title</Trans>:
<ol className="leading-loose">
<li>
<code>1.1.1.1</code> - { t('example_upstream_regular') }
</li>
<li>
<code>tls://1dot1dot1dot1.cloudflare-dns.com</code> - <span dangerouslySetInnerHTML={{ __html: t('example_upstream_dot') }} />
</li>
<li>
<code>https://cloudflare-dns.com/dns-query</code> - <span dangerouslySetInnerHTML={{ __html: t('example_upstream_doh') }} />
</li>
<li>
<code>tcp://1.1.1.1</code> - { t('example_upstream_tcp') }
</li>
<li>
<code>sdns://...</code> - <span dangerouslySetInnerHTML={{ __html: t('example_upstream_sdns') }} />
</li>
</ol>
</div>
</div>
</div>
</Card>
);
}
}
Upstream.propTypes = {
upstreamDns: PropTypes.string,
processingTestUpstream: PropTypes.bool,
handleUpstreamChange: PropTypes.func,
handleUpstreamSubmit: PropTypes.func,
handleUpstreamTest: PropTypes.func,
t: PropTypes.func,
};
export default withNamespaces()(Upstream);

View file

@ -0,0 +1,32 @@
import React from 'react';
import PropTypes from 'prop-types';
import { Trans, withNamespaces } from 'react-i18next';
const Examples = props => (
<div className="list leading-loose">
<Trans>examples_title</Trans>:
<ol className="leading-loose">
<li>
<code>1.1.1.1</code> - { props.t('example_upstream_regular') }
</li>
<li>
<code>tls://1dot1dot1dot1.cloudflare-dns.com</code> - <span dangerouslySetInnerHTML={{ __html: props.t('example_upstream_dot') }} />
</li>
<li>
<code>https://cloudflare-dns.com/dns-query</code> - <span dangerouslySetInnerHTML={{ __html: props.t('example_upstream_doh') }} />
</li>
<li>
<code>tcp://1.1.1.1</code> - { props.t('example_upstream_tcp') }
</li>
<li>
<code>sdns://...</code> - <span dangerouslySetInnerHTML={{ __html: props.t('example_upstream_sdns') }} />
</li>
</ol>
</div>
);
Examples.propTypes = {
t: PropTypes.func.isRequired,
};
export default withNamespaces()(Examples);

View file

@ -0,0 +1,120 @@
import React from 'react';
import { connect } from 'react-redux';
import PropTypes from 'prop-types';
import { Field, reduxForm, formValueSelector } from 'redux-form';
import { Trans, withNamespaces } from 'react-i18next';
import flow from 'lodash/flow';
import classnames from 'classnames';
import { renderSelectField } from '../../../helpers/form';
let Form = (props) => {
const {
t,
handleSubmit,
testUpstream,
upstreamDns,
submitting,
invalid,
processingSetUpstream,
processingTestUpstream,
} = props;
const testButtonClass = classnames({
'btn btn-primary btn-standard mr-2': true,
'btn btn-primary btn-standard mr-2 btn-loading': processingTestUpstream,
});
return (
<form onSubmit={handleSubmit}>
<div className="row">
<div className="col-12">
<div className="form__group form__group--settings">
<label>{t('upstream_dns')}</label>
<Field
id="upstream_dns"
name="upstream_dns"
component="textarea"
type="text"
className="form-control form-control--textarea"
placeholder={t('upstream_dns')}
/>
</div>
</div>
<div className="col-12">
<div className="form__group form__group--settings">
<Field
name="all_servers"
type="checkbox"
component={renderSelectField}
placeholder={t('upstream_parallel')}
/>
</div>
</div>
<div className="col-12">
<div className="form__group">
<label>{t('bootstrap_dns')}</label>
<Field
id="bootstrap_dns"
name="bootstrap_dns"
component="textarea"
type="text"
className="form-control"
placeholder={t('bootstrap_dns_desc')}
/>
</div>
</div>
</div>
<div className="card-actions">
<div className="btn-list">
<button
type="button"
className={testButtonClass}
onClick={() => testUpstream(upstreamDns)}
disabled={!upstreamDns || processingTestUpstream}
>
<Trans>test_upstream_btn</Trans>
</button>
<button
type="submit"
className="btn btn-success btn-standard"
disabled={
submitting
|| invalid
|| processingSetUpstream
|| processingTestUpstream
}
>
<Trans>apply_btn</Trans>
</button>
</div>
</div>
</form>
);
};
Form.propTypes = {
handleSubmit: PropTypes.func,
testUpstream: PropTypes.func,
submitting: PropTypes.bool,
invalid: PropTypes.bool,
initialValues: PropTypes.object,
upstreamDns: PropTypes.string,
processingTestUpstream: PropTypes.bool,
processingSetUpstream: PropTypes.bool,
t: PropTypes.func,
};
const selector = formValueSelector('upstreamForm');
Form = connect((state) => {
const upstreamDns = selector(state, 'upstream_dns');
return {
upstreamDns,
};
})(Form);
export default flow([
withNamespaces(),
reduxForm({ form: 'upstreamForm' }),
])(Form);

View file

@ -0,0 +1,67 @@
import React, { Component } from 'react';
import PropTypes from 'prop-types';
import { withNamespaces } from 'react-i18next';
import Form from './Form';
import Examples from './Examples';
import Card from '../../ui/Card';
class Upstream extends Component {
handleSubmit = (values) => {
this.props.setUpstream(values);
};
handleTest = (values) => {
this.props.testUpstream(values);
}
render() {
const {
t,
upstreamDns: upstream_dns,
bootstrapDns: bootstrap_dns,
allServers: all_servers,
processingSetUpstream,
processingTestUpstream,
} = this.props;
return (
<Card
title={ t('upstream_dns') }
subtitle={ t('upstream_dns_hint') }
bodyType="card-body box-body--settings"
>
<div className="row">
<div className="col">
<Form
initialValues={{
upstream_dns,
bootstrap_dns,
all_servers,
}}
testUpstream={this.handleTest}
onSubmit={this.handleSubmit}
processingTestUpstream={processingTestUpstream}
processingSetUpstream={processingSetUpstream}
/>
<hr/>
<Examples />
</div>
</div>
</Card>
);
}
}
Upstream.propTypes = {
upstreamDns: PropTypes.string,
bootstrapDns: PropTypes.string,
allServers: PropTypes.bool,
setUpstream: PropTypes.func.isRequired,
testUpstream: PropTypes.func.isRequired,
processingSetUpstream: PropTypes.bool.isRequired,
processingTestUpstream: PropTypes.bool.isRequired,
t: PropTypes.func.isRequired,
};
export default withNamespaces()(Upstream);

View file

@ -41,22 +41,6 @@ class Settings extends Component {
this.props.getTlsStatus();
}
handleUpstreamChange = (value) => {
this.props.handleUpstreamChange({ upstreamDns: value });
};
handleUpstreamSubmit = () => {
this.props.setUpstream(this.props.dashboard.upstreamDns);
};
handleUpstreamTest = () => {
if (this.props.dashboard.upstreamDns.length > 0) {
this.props.testUpstream(this.props.dashboard.upstreamDns);
} else {
this.props.addErrorToast({ error: this.props.t('no_servers_specified') });
}
};
renderSettings = (settings) => {
if (Object.keys(settings).length > 0) {
return Object.keys(settings).map((key) => {
@ -75,8 +59,7 @@ class Settings extends Component {
}
render() {
const { settings, t } = this.props;
const { upstreamDns } = this.props.dashboard;
const { settings, dashboard, t } = this.props;
return (
<Fragment>
<PageTitle title={ t('settings') } />
@ -91,11 +74,13 @@ class Settings extends Component {
</div>
</Card>
<Upstream
upstreamDns={upstreamDns}
upstreamDns={dashboard.upstreamDns}
boostrapDns={dashboard.boostrapDns}
allServers={dashboard.allServers}
setUpstream={this.props.setUpstream}
testUpstream={this.props.testUpstream}
processingTestUpstream={settings.processingTestUpstream}
handleUpstreamChange={this.handleUpstreamChange}
handleUpstreamSubmit={this.handleUpstreamSubmit}
handleUpstreamTest={this.handleUpstreamTest}
processingSetUpstream={settings.processingSetUpstream}
/>
<Encryption
encryption={this.props.encryption}

View file

@ -91,6 +91,10 @@
line-height: 1.5;
}
.checkbox__label-text--long {
max-width: initial;
}
.checkbox__label-title {
display: block;
line-height: 1.5;

View file

@ -32,7 +32,7 @@ export const renderSelectField = ({
disabled={disabled}
/>
<span className="checkbox__label">
<span className="checkbox__label-text">
<span className="checkbox__label-text checkbox__label-text--long">
<span className="checkbox__label-title">{placeholder}</span>
</span>
</span>

View file

@ -201,3 +201,5 @@ export const redirectToCurrentProtocol = (values, httpPort = 80) => {
window.location.replace(`http://${hostname}:${httpPort}/${hash}`);
}
};
export const normalizeTextarea = text => text && text.replace(/[;, ]/g, '\n').split('\n');

View file

@ -51,6 +51,8 @@ const dashboard = handleActions({
dns_address: dnsAddress,
querylog_enabled: queryLogEnabled,
upstream_dns: upstreamDns,
bootstrap_dns: bootstrapDns,
all_servers: allServers,
protection_enabled: protectionEnabled,
language,
http_port: httpPort,
@ -64,6 +66,8 @@ const dashboard = handleActions({
dnsAddress,
queryLogEnabled,
upstreamDns: upstreamDns.join('\n'),
bootstrapDns: bootstrapDns.join('\n'),
allServers,
protectionEnabled,
language,
httpPort,
@ -171,7 +175,9 @@ const dashboard = handleActions({
logStatusProcessing: false,
processingVersion: true,
processingFiltering: true,
upstreamDns: [],
upstreamDns: '',
bootstrapDns: '',
allServers: false,
protectionEnabled: false,
processingProtection: false,
httpPort: 80,