diff --git a/client/package-lock.json b/client/package-lock.json index fc6ea4f1..585d2b4c 100644 --- a/client/package-lock.json +++ b/client/package-lock.json @@ -4126,6 +4126,11 @@ "next-tick": "1" } }, + "es6-error": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/es6-error/-/es6-error-4.1.1.tgz", + "integrity": "sha512-Um/+FxMr9CISWh0bi5Zv0iOD+4cFh5qLeks1qhAopKVAJw3drgKbKySikp7wGhDL0HPeaja0P5ULZrxLkniUVg==" + }, "es6-iterator": { "version": "2.0.3", "resolved": "https://registry.npmjs.org/es6-iterator/-/es6-iterator-2.0.3.tgz", @@ -6588,7 +6593,7 @@ }, "html-webpack-plugin": { "version": "3.2.0", - "resolved": "https://registry.npmjs.org/html-webpack-plugin/-/html-webpack-plugin-3.2.0.tgz", + "resolved": "http://registry.npmjs.org/html-webpack-plugin/-/html-webpack-plugin-3.2.0.tgz", "integrity": "sha1-sBq71yOsqqeze2r0SS69oD2d03s=", "dev": true, "requires": { @@ -6638,7 +6643,7 @@ }, "readable-stream": { "version": "1.0.34", - "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-1.0.34.tgz", + "resolved": "http://registry.npmjs.org/readable-stream/-/readable-stream-1.0.34.tgz", "integrity": "sha1-Elgg40vIQtLyqq+v5MKRbuMsFXw=", "dev": true, "requires": { @@ -7387,8 +7392,7 @@ "is-promise": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/is-promise/-/is-promise-2.1.0.tgz", - "integrity": "sha1-eaKp7OfwlugPNtKy87wWwf9L8/o=", - "dev": true + "integrity": "sha1-eaKp7OfwlugPNtKy87wWwf9L8/o=" }, "is-regex": { "version": "1.0.4", @@ -13202,6 +13206,21 @@ "reduce-reducers": "^0.1.0" } }, + "redux-form": { + "version": "7.4.2", + "resolved": "https://registry.npmjs.org/redux-form/-/redux-form-7.4.2.tgz", + "integrity": "sha512-QxC36s4Lelx5Cr8dbpxqvl23dwYOydeAX8c6YPmgkz/Dhj053C16S2qoyZN6LO6HJ2oUF00rKsAyE94GwOUhFA==", + "requires": { + "es6-error": "^4.1.1", + "hoist-non-react-statics": "^2.5.4", + "invariant": "^2.2.4", + "is-promise": "^2.1.0", + "lodash": "^4.17.10", + "lodash-es": "^4.17.10", + "prop-types": "^15.6.1", + "react-lifecycles-compat": "^3.0.4" + } + }, "redux-thunk": { "version": "2.3.0", "resolved": "https://registry.npmjs.org/redux-thunk/-/redux-thunk-2.3.0.tgz", @@ -15003,7 +15022,7 @@ }, "through": { "version": "2.3.8", - "resolved": "https://registry.npmjs.org/through/-/through-2.3.8.tgz", + "resolved": "http://registry.npmjs.org/through/-/through-2.3.8.tgz", "integrity": "sha1-DdTJ/6q8NXlgsbckEV1+Doai4fU=", "dev": true }, diff --git a/client/package.json b/client/package.json index 7d200e80..6b518d60 100644 --- a/client/package.json +++ b/client/package.json @@ -31,6 +31,7 @@ "react-transition-group": "^2.4.0", "redux": "^4.0.0", "redux-actions": "^2.4.0", + "redux-form": "^7.4.2", "redux-thunk": "^2.3.0", "svg-url-loader": "^2.3.2", "whatwg-fetch": "2.0.3" diff --git a/client/src/__locales/en.json b/client/src/__locales/en.json index f6045521..d20555fb 100644 --- a/client/src/__locales/en.json +++ b/client/src/__locales/en.json @@ -127,5 +127,16 @@ "category_label": "Category", "rule_label": "Rule", "filter_label": "Filter", - "unknown_filter": "Unknown filter {{filterId}}" -} \ No newline at end of file + "unknown_filter": "Unknown filter {{filterId}}", + "refresh_status": "Refresh status", + "save_config": "Save config", + "enabled_dhcp": "DHCP server enabled", + "disabled_dhcp": "DHCP server disabled", + "dhcp_title": "DHCP server", + "dhcp_description": "If your router does not provide DHCP settings, you can use AdGuard's own built-in DHCP server.", + "dhcp_enable": "Enable DHCP server", + "dhcp_disable": "Disable DHCP server", + "dhcp_not_found": "No active DHCP servers found on the network. It is safe to enable the built-in DHCP server.", + "dhcp_leases": "DHCP leases", + "dhcp_leases_not_found": "No DHCP leases found" +} diff --git a/client/src/actions/index.js b/client/src/actions/index.js index a4da8c9a..cd56647e 100644 --- a/client/src/actions/index.js +++ b/client/src/actions/index.js @@ -522,3 +522,73 @@ export const getLanguage = () => async (dispatch) => { dispatch(getLanguageFailure()); } }; + +export const getDhcpStatusRequest = createAction('GET_DHCP_STATUS_REQUEST'); +export const getDhcpStatusSuccess = createAction('GET_DHCP_STATUS_SUCCESS'); +export const getDhcpStatusFailure = createAction('GET_DHCP_STATUS_FAILURE'); + +export const getDhcpStatus = () => async (dispatch) => { + dispatch(getDhcpStatusRequest()); + try { + const status = await apiClient.getDhcpStatus(); + dispatch(getDhcpStatusSuccess(status)); + } catch (error) { + dispatch(addErrorToast({ error })); + dispatch(getDhcpStatusFailure()); + } +}; + +export const setDhcpConfigRequest = createAction('SET_DHCP_CONFIG_REQUEST'); +export const setDhcpConfigSuccess = createAction('SET_DHCP_CONFIG_SUCCESS'); +export const setDhcpConfigFailure = createAction('SET_DHCP_CONFIG_FAILURE'); + +export const setDhcpConfig = config => async (dispatch) => { + dispatch(setDhcpConfigRequest()); + try { + await apiClient.setDhcpConfig(config); + dispatch(setDhcpConfigSuccess()); + } catch (error) { + dispatch(addErrorToast({ error })); + dispatch(setDhcpConfigFailure()); + } +}; + +export const findActiveDhcpRequest = createAction('FIND_ACTIVE_DHCP_REQUEST'); +export const findActiveDhcpSuccess = createAction('FIND_ACTIVE_DHCP_SUCCESS'); +export const findActiveDhcpFailure = createAction('FIND_ACTIVE_DHCP_FAILURE'); + +export const findActiveDhcp = () => async (dispatch) => { + dispatch(findActiveDhcpRequest()); + try { + const result = await apiClient.findActiveDhcp(); + dispatch(findActiveDhcpSuccess(result)); + } catch (error) { + dispatch(addErrorToast({ error })); + dispatch(findActiveDhcpFailure()); + } +}; + +export const toggleDhcpRequest = createAction('TOGGLE_DHCP_REQUEST'); +export const toggleDhcpFailure = createAction('TOGGLE_DHCP_FAILURE'); +export const toggleDhcpSuccess = createAction('TOGGLE_DHCP_SUCCESS'); + +export const toggleDhcp = status => async (dispatch) => { + dispatch(toggleDhcpRequest()); + let successMessage = ''; + + try { + if (status) { + successMessage = 'disabled_dhcp'; + await apiClient.disableGlobalProtection(); + } else { + successMessage = 'enabled_dhcp'; + await apiClient.enableGlobalProtection(); + } + + dispatch(addSuccessToast(successMessage)); + dispatch(toggleDhcpSuccess()); + } catch (error) { + dispatch(addErrorToast({ error })); + dispatch(toggleDhcpFailure()); + } +}; diff --git a/client/src/api/Api.js b/client/src/api/Api.js index f0d90941..966b5199 100644 --- a/client/src/api/Api.js +++ b/client/src/api/Api.js @@ -302,4 +302,71 @@ export default class Api { }; return this.makeRequest(path, method, parameters); } + + // DHCP + DHCP_STATUS = { path: 'dhcp/status', method: 'GET' }; + DHCP_SET_CONFIG = { path: 'dhcp/set_config', method: 'POST' }; + DHCP_FIND_ACTIVE = { path: 'dhcp/find_active_dhcp', method: 'GET' }; + + getDhcpStatus() { + // const { path, method } = this.DHCP_STATUS; + // return this.makeRequest(path, method); + + const example = { + config: { + enabled: false, + gateway_ip: '192.168.1.1', + subnet_mask: '255.255.255.0', + range_start: '192.168.1.2', + range_end: '192.168.10.50', + lease_duration: '43200', + }, + leases: [ + { + mac: '001109b3b3b8', + ip: '192.168.1.22', + hostname: 'dell', + expires: '2017-07-21T17:32:28Z', + }, + { + mac: '001109b3b3b9', + ip: '192.168.1.23', + hostname: 'dell', + expires: '2017-07-21T17:32:28Z', + }, + ], + }; + + return new Promise((resolve) => { + setTimeout(() => { + resolve(example); + }, 1000); + }); + } + + setDhcpConfig(config) { + // const { path, method } = this.DHCP_SET_CONFIG; + // const parameters = config; + // return this.makeRequest(path, method, parameters); + + return new Promise((resolve) => { + setTimeout(() => { + resolve(window.alert(`Set config:\n\n${JSON.stringify(config, null, 2)}`)); + }, 1000); + }); + } + + findActiveDhcp() { + // const { path, method } = this.DHCP_FIND_ACTIVE; + // return this.makeRequest(path, method); + + return new Promise((resolve) => { + setTimeout(() => { + resolve({ + gateway_ip: '127.0.0.1', + found: true, + }); + }, 10000); + }); + } } diff --git a/client/src/components/Settings/Dhcp/Form.js b/client/src/components/Settings/Dhcp/Form.js new file mode 100644 index 00000000..a4caea0b --- /dev/null +++ b/client/src/components/Settings/Dhcp/Form.js @@ -0,0 +1,134 @@ +import React, { Fragment } from 'react'; +import PropTypes from 'prop-types'; +import { Field, reduxForm } from 'redux-form'; +import { R_IPV4 } from '../../../helpers/constants'; + +const required = (value) => { + if (value) { + return false; + } + return 'Required field'; +}; + +const ipv4 = (value) => { + if (value && !new RegExp(R_IPV4).test(value)) { + return 'Invalid IPv4 format'; + } + return false; +}; + +const renderField = ({ + input, className, placeholder, type, disabled, meta: { touched, error }, +}) => ( + + + {!disabled && touched && (error && {error})} + +); + +const Form = (props) => { + const { + handleSubmit, pristine, submitting, enabled, + } = props; + + return ( +
+
+
+
+ + +
+
+ + +
+
+
+
+
+
+ +
+
+ +
+
+ +
+
+
+
+ + +
+
+
+ + +
+ ); +}; + +Form.propTypes = { + handleSubmit: PropTypes.func, + pristine: PropTypes.bool, + submitting: PropTypes.bool, + enabled: PropTypes.bool, +}; + +export default reduxForm({ + form: 'dhcpForm', +})(Form); diff --git a/client/src/components/Settings/Dhcp/Leases.js b/client/src/components/Settings/Dhcp/Leases.js new file mode 100644 index 00000000..89959946 --- /dev/null +++ b/client/src/components/Settings/Dhcp/Leases.js @@ -0,0 +1,36 @@ +import React from 'react'; +import PropTypes from 'prop-types'; +import ReactTable from 'react-table'; +import { withNamespaces } from 'react-i18next'; + +const columns = [{ + Header: 'MAC', + accessor: 'mac', +}, { + Header: 'IP', + accessor: 'ip', +}, { + Header: 'Hostname', + accessor: 'hostname', +}, { + Header: 'Expires', + accessor: 'expires', +}]; + +const Leases = props => ( + +); + +Leases.propTypes = { + leases: PropTypes.array, + t: PropTypes.func, +}; + +export default withNamespaces()(Leases); diff --git a/client/src/components/Settings/Dhcp/index.js b/client/src/components/Settings/Dhcp/index.js new file mode 100644 index 00000000..2b584bfb --- /dev/null +++ b/client/src/components/Settings/Dhcp/index.js @@ -0,0 +1,93 @@ +import React, { Component, Fragment } from 'react'; +import PropTypes from 'prop-types'; +import classnames from 'classnames'; +import { Trans, withNamespaces } from 'react-i18next'; + +import Form from './Form'; +import Leases from './Leases'; +import Card from '../../ui/Card'; + +class Dhcp extends Component { + handleFormSubmit = (values) => { + this.props.setDhcpConfig(values); + }; + + handleRefresh = () => { + this.props.findActiveDhcp(); + } + + getToggleDhcpButton = () => { + const { enabled } = this.props.dhcp.config; + const buttonText = enabled ? 'dhcp_disable' : 'dhcp_enable'; + const buttonClass = enabled ? 'btn-gray' : 'btn-success'; + + return ( + + ); + } + + render() { + const { t, dhcp } = this.props; + const statusButtonClass = classnames({ + 'btn btn-primary btn-standart': true, + 'btn btn-primary btn-standart btn-loading': dhcp.processingStatus, + }); + + return ( + + {!dhcp.processing && + +
+
+
+ {this.getToggleDhcpButton()} + +
+ {dhcp.active && !dhcp.active.found && +
+ dhcp_not_found +
+ } +
+
+
+
+
+ } + {!dhcp.processing && dhcp.config.enabled && + +
+
+ +
+
+
+ } +
+ ); + } +} + +Dhcp.propTypes = { + dhcp: PropTypes.object, + toggleDhcp: PropTypes.func, + getDhcpStatus: PropTypes.func, + setDhcpConfig: PropTypes.func, + findActiveDhcp: PropTypes.func, + handleSubmit: PropTypes.func, + t: PropTypes.func, +}; + +export default withNamespaces()(Dhcp); diff --git a/client/src/components/Settings/Settings.css b/client/src/components/Settings/Settings.css index 380a70f6..f9b50007 100644 --- a/client/src/components/Settings/Settings.css +++ b/client/src/components/Settings/Settings.css @@ -1,4 +1,5 @@ .form__group { + position: relative; margin-bottom: 15px; } @@ -6,6 +7,10 @@ margin-bottom: 0; } +.form__group--dhcp:last-child { + margin-bottom: 15px; +} + .btn-standart { padding-left: 20px; padding-right: 20px; @@ -18,3 +23,11 @@ .form-control--textarea-large { min-height: 240px; } + +.form__message { + font-size: 11px; +} + +.form__message--error { + color: #cd201f; +} diff --git a/client/src/components/Settings/index.js b/client/src/components/Settings/index.js index 74a02a20..aafc64e2 100644 --- a/client/src/components/Settings/index.js +++ b/client/src/components/Settings/index.js @@ -2,6 +2,7 @@ import React, { Component, Fragment } from 'react'; import PropTypes from 'prop-types'; import { withNamespaces, Trans } from 'react-i18next'; import Upstream from './Upstream'; +import Dhcp from './Dhcp'; import Checkbox from '../ui/Checkbox'; import Loading from '../ui/Loading'; import PageTitle from '../ui/PageTitle'; @@ -34,6 +35,7 @@ class Settings extends Component { componentDidMount() { this.props.initSettings(this.settings); + this.props.getDhcpStatus(); } handleUpstreamChange = (value) => { @@ -92,6 +94,13 @@ class Settings extends Component { handleUpstreamSubmit={this.handleUpstreamSubmit} handleUpstreamTest={this.handleUpstreamTest} /> + diff --git a/client/src/containers/Settings.js b/client/src/containers/Settings.js index 144e968a..062fc2f2 100644 --- a/client/src/containers/Settings.js +++ b/client/src/containers/Settings.js @@ -1,10 +1,21 @@ import { connect } from 'react-redux'; -import { initSettings, toggleSetting, handleUpstreamChange, setUpstream, testUpstream, addErrorToast } from '../actions'; +import { + initSettings, + toggleSetting, + handleUpstreamChange, + setUpstream, + testUpstream, + addErrorToast, + toggleDhcp, + getDhcpStatus, + setDhcpConfig, + findActiveDhcp, +} from '../actions'; import Settings from '../components/Settings'; const mapStateToProps = (state) => { - const { settings, dashboard } = state; - const props = { settings, dashboard }; + const { settings, dashboard, dhcp } = state; + const props = { settings, dashboard, dhcp }; return props; }; @@ -15,6 +26,10 @@ const mapDispatchToProps = { setUpstream, testUpstream, addErrorToast, + toggleDhcp, + getDhcpStatus, + setDhcpConfig, + findActiveDhcp, }; export default connect( diff --git a/client/src/helpers/constants.js b/client/src/helpers/constants.js index 1fe9348a..f0b2aea7 100644 --- a/client/src/helpers/constants.js +++ b/client/src/helpers/constants.js @@ -1,4 +1,5 @@ export const R_URL_REQUIRES_PROTOCOL = /^https?:\/\/\w[\w_\-.]*\.[a-z]{2,8}[^\s]*$/; +export const R_IPV4 = /^(?:(?:^|\.)(?:2(?:5[0-5]|[0-4]\d)|1?\d?\d)){4}$/g; export const STATS_NAMES = { avg_processing_time: 'average_processing_time', diff --git a/client/src/reducers/index.js b/client/src/reducers/index.js index 9a54f249..0702a06a 100644 --- a/client/src/reducers/index.js +++ b/client/src/reducers/index.js @@ -2,6 +2,7 @@ import { combineReducers } from 'redux'; import { handleActions } from 'redux-actions'; import { loadingBarReducer } from 'react-redux-loading-bar'; import nanoid from 'nanoid'; +import { reducer as formReducer } from 'redux-form'; import versionCompare from '../helpers/versionCompare'; import * as actions from '../actions'; @@ -35,6 +36,7 @@ const settings = handleActions({ processing: true, processingTestUpstream: false, processingSetUpstream: false, + processingDhcpStatus: false, }); const dashboard = handleActions({ @@ -258,11 +260,44 @@ const toasts = handleActions({ }, }, { notices: [] }); +const dhcp = handleActions({ + [actions.getDhcpStatusRequest]: state => ({ ...state, processing: true }), + [actions.getDhcpStatusFailure]: state => ({ ...state, processing: false }), + [actions.getDhcpStatusSuccess]: (state, { payload }) => { + const newState = { + ...state, + ...payload, + processing: false, + }; + return newState; + }, + + [actions.findActiveDhcpRequest]: state => ({ ...state, processingStatus: true }), + [actions.findActiveDhcpFailure]: state => ({ ...state, processingStatus: false }), + [actions.findActiveDhcpSuccess]: (state, { payload }) => ({ + ...state, + active: payload, + processingStatus: false, + }), + + [actions.toggleDhcpSuccess]: (state) => { + const { config } = state; + const newConfig = { ...config, enabled: !config.enabled }; + const newState = { ...state, config: newConfig }; + return newState; + }, +}, { + processing: true, + processingStatus: false, +}); + export default combineReducers({ settings, dashboard, queryLogs, filtering, toasts, + dhcp, loadingBar: loadingBarReducer, + form: formReducer, });