From 5d6c980ac7a33e5a4b8d3c376d8984e36cba1f83 Mon Sep 17 00:00:00 2001 From: Ildar Kamalov <i.kamalov@adguard.com> Date: Wed, 6 Mar 2019 14:45:21 +0300 Subject: [PATCH] * client: upstream form --- client/src/__locales/en.json | 5 +- client/src/actions/index.js | 13 +- client/src/api/Api.js | 6 +- client/src/components/Settings/Upstream.js | 97 -------------- .../components/Settings/Upstream/Examples.js | 32 +++++ .../src/components/Settings/Upstream/Form.js | 120 ++++++++++++++++++ .../src/components/Settings/Upstream/index.js | 67 ++++++++++ client/src/components/Settings/index.js | 29 +---- client/src/components/ui/Checkbox.css | 4 + client/src/helpers/form.js | 2 +- client/src/helpers/helpers.js | 2 + client/src/reducers/index.js | 8 +- 12 files changed, 256 insertions(+), 129 deletions(-) delete mode 100644 client/src/components/Settings/Upstream.js create mode 100644 client/src/components/Settings/Upstream/Examples.js create mode 100644 client/src/components/Settings/Upstream/Form.js create mode 100644 client/src/components/Settings/Upstream/index.js diff --git a/client/src/__locales/en.json b/client/src/__locales/en.json index 45c33828..b70cdd37 100644 --- a/client/src/__locales/en.json +++ b/client/src/__locales/en.json @@ -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" } diff --git a/client/src/actions/index.js b/client/src/actions/index.js index 1bb99064..0bb99940 100644 --- a/client/src/actions/index.js +++ b/client/src/actions/index.js @@ -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) => { diff --git a/client/src/api/Api.js b/client/src/api/Api.js index d20a334a..17df1221 100644 --- a/client/src/api/Api.js +++ b/client/src/api/Api.js @@ -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); } diff --git a/client/src/components/Settings/Upstream.js b/client/src/components/Settings/Upstream.js deleted file mode 100644 index fe24c4d7..00000000 --- a/client/src/components/Settings/Upstream.js +++ /dev/null @@ -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); diff --git a/client/src/components/Settings/Upstream/Examples.js b/client/src/components/Settings/Upstream/Examples.js new file mode 100644 index 00000000..4ba54852 --- /dev/null +++ b/client/src/components/Settings/Upstream/Examples.js @@ -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); diff --git a/client/src/components/Settings/Upstream/Form.js b/client/src/components/Settings/Upstream/Form.js new file mode 100644 index 00000000..7a38b31a --- /dev/null +++ b/client/src/components/Settings/Upstream/Form.js @@ -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); diff --git a/client/src/components/Settings/Upstream/index.js b/client/src/components/Settings/Upstream/index.js new file mode 100644 index 00000000..f622268c --- /dev/null +++ b/client/src/components/Settings/Upstream/index.js @@ -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); diff --git a/client/src/components/Settings/index.js b/client/src/components/Settings/index.js index efa49bdd..6f0be6b1 100644 --- a/client/src/components/Settings/index.js +++ b/client/src/components/Settings/index.js @@ -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} diff --git a/client/src/components/ui/Checkbox.css b/client/src/components/ui/Checkbox.css index 9d483a5d..0d596a77 100644 --- a/client/src/components/ui/Checkbox.css +++ b/client/src/components/ui/Checkbox.css @@ -91,6 +91,10 @@ line-height: 1.5; } +.checkbox__label-text--long { + max-width: initial; +} + .checkbox__label-title { display: block; line-height: 1.5; diff --git a/client/src/helpers/form.js b/client/src/helpers/form.js index 1f0a339a..c9703033 100644 --- a/client/src/helpers/form.js +++ b/client/src/helpers/form.js @@ -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> diff --git a/client/src/helpers/helpers.js b/client/src/helpers/helpers.js index eb7c7db2..def63dce 100644 --- a/client/src/helpers/helpers.js +++ b/client/src/helpers/helpers.js @@ -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'); diff --git a/client/src/reducers/index.js b/client/src/reducers/index.js index e83f48a1..90ace4d6 100644 --- a/client/src/reducers/index.js +++ b/client/src/reducers/index.js @@ -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,