From a6d6e9ec9ef0f6c13e88f1785366ba8e5cf08156 Mon Sep 17 00:00:00 2001 From: Ildar Kamalov Date: Thu, 28 Nov 2019 14:47:06 +0300 Subject: [PATCH] + client: add multiple fields client form --- client/src/__locales/en.json | 3 +- client/src/actions/clients.js | 25 +-- client/src/api/Api.js | 7 + .../Settings/Clients/ClientsTable.js | 43 ++-- .../src/components/Settings/Clients/Form.js | 201 +++++++++++------- client/src/components/ui/Icons.css | 5 + client/src/components/ui/Icons.js | 8 + client/src/helpers/form.js | 45 ++++ client/src/helpers/formatClientCell.js | 4 +- client/src/helpers/helpers.js | 14 ++ 10 files changed, 220 insertions(+), 135 deletions(-) diff --git a/client/src/__locales/en.json b/client/src/__locales/en.json index 764d1450..4e834147 100644 --- a/client/src/__locales/en.json +++ b/client/src/__locales/en.json @@ -299,9 +299,10 @@ "client_edit": "Edit Client", "client_identifier": "Identifier", "ip_address": "IP address", - "client_identifier_desc": "Clients can be identified by the IP address or MAC address. Please note, that using MAC as identifier is possible only if AdGuard Home is also a <0>DHCP server", + "client_identifier_desc": "Clients can be identified by the IP address, MAC address, CIDR. Please note, that using MAC as identifier is possible only if AdGuard Home is also a <0>DHCP server", "form_enter_ip": "Enter IP", "form_enter_mac": "Enter MAC", + "form_enter_id": "Enter identifier", "form_client_name": "Enter client name", "client_global_settings": "Use global settings", "client_deleted": "Client \"{{key}}\" successfully deleted", diff --git a/client/src/actions/clients.js b/client/src/actions/clients.js index 3974a38c..b6fcf011 100644 --- a/client/src/actions/clients.js +++ b/client/src/actions/clients.js @@ -2,7 +2,6 @@ import { createAction } from 'redux-actions'; import { t } from 'i18next'; import apiClient from '../api/Api'; import { addErrorToast, addSuccessToast, getClients } from './index'; -import { CLIENT_ID } from '../helpers/constants'; export const toggleClientModal = createAction('TOGGLE_CLIENT_MODAL'); @@ -13,18 +12,7 @@ export const addClientSuccess = createAction('ADD_CLIENT_SUCCESS'); export const addClient = config => async (dispatch) => { dispatch(addClientRequest()); try { - let data; - if (config.identifier === CLIENT_ID.MAC) { - const { ip, identifier, ...values } = config; - - data = { ...values }; - } else { - const { mac, identifier, ...values } = config; - - data = { ...values }; - } - - await apiClient.addClient(data); + await apiClient.addClient(config); dispatch(addClientSuccess()); dispatch(toggleClientModal()); dispatch(addSuccessToast(t('client_added', { key: config.name }))); @@ -59,16 +47,7 @@ export const updateClientSuccess = createAction('UPDATE_CLIENT_SUCCESS'); export const updateClient = (config, name) => async (dispatch) => { dispatch(updateClientRequest()); try { - let data; - if (config.identifier === CLIENT_ID.MAC) { - const { ip, identifier, ...values } = config; - - data = { name, data: { ...values } }; - } else { - const { mac, identifier, ...values } = config; - - data = { name, data: { ...values } }; - } + const data = { name, data: { ...config } }; await apiClient.updateClient(data); dispatch(updateClientSuccess()); diff --git a/client/src/api/Api.js b/client/src/api/Api.js index 470577a8..72d6d527 100644 --- a/client/src/api/Api.js +++ b/client/src/api/Api.js @@ -353,6 +353,7 @@ class Api { // Per-client settings GET_CLIENTS = { path: 'clients', method: 'GET' }; + FIND_CLIENTS = { path: 'clients/find', method: 'GET' }; ADD_CLIENT = { path: 'clients/add', method: 'POST' }; DELETE_CLIENT = { path: 'clients/delete', method: 'POST' }; UPDATE_CLIENT = { path: 'clients/update', method: 'POST' }; @@ -389,6 +390,12 @@ class Api { return this.makeRequest(path, method, parameters); } + findClients(params) { + const { path, method } = this.FIND_CLIENTS; + const url = getPathWithQueryString(path, params); + return this.makeRequest(url, method); + } + // DNS access settings ACCESS_LIST = { path: 'access/list', method: 'GET' }; ACCESS_SET = { path: 'access/set', method: 'POST' }; diff --git a/client/src/components/Settings/Clients/ClientsTable.js b/client/src/components/Settings/Clients/ClientsTable.js index de1c166f..50bb48ab 100644 --- a/client/src/components/Settings/Clients/ClientsTable.js +++ b/client/src/components/Settings/Clients/ClientsTable.js @@ -3,7 +3,7 @@ import PropTypes from 'prop-types'; import { Trans, withNamespaces } from 'react-i18next'; import ReactTable from 'react-table'; -import { MODAL_TYPE, CLIENT_ID } from '../../../helpers/constants'; +import { MODAL_TYPE } from '../../../helpers/constants'; import Card from '../../ui/Card'; import Modal from './Modal'; import WrapCell from './WrapCell'; @@ -40,10 +40,7 @@ class ClientsTable extends Component { const client = clients.find(item => name === item.name); if (client) { - const identifier = client.mac ? CLIENT_ID.MAC : CLIENT_ID.IP; - return { - identifier, use_global_settings: true, use_global_blocked_services: true, ...client, @@ -51,7 +48,7 @@ class ClientsTable extends Component { } return { - identifier: CLIENT_ID.IP, + ids: [''], use_global_settings: true, use_global_blocked_services: true, }; @@ -76,28 +73,22 @@ class ClientsTable extends Component { columns = [ { Header: this.props.t('table_client'), - accessor: 'ip', + accessor: 'ids', minWidth: 150, Cell: (row) => { - if (row.original && row.original.mac) { - return ( -
- - {row.original.mac} (MAC) - -
- ); - } else if (row.value) { - return ( -
- - {row.value} (IP) - -
- ); - } + const { value } = row; - return ''; + return ( +
+ + {value.map(address => ( +
+ {address} +
+ ))} +
+
+ ); }, }, { @@ -119,9 +110,7 @@ class ClientsTable extends Component { return (
-
- {title} -
+
{title}
); }, diff --git a/client/src/components/Settings/Clients/Form.js b/client/src/components/Settings/Clients/Form.js index 2bf6679f..6e3ceced 100644 --- a/client/src/components/Settings/Clients/Form.js +++ b/client/src/components/Settings/Clients/Form.js @@ -1,14 +1,20 @@ import React from 'react'; import { connect } from 'react-redux'; import PropTypes from 'prop-types'; -import { Field, reduxForm, formValueSelector } from 'redux-form'; +import { Field, FieldArray, reduxForm, formValueSelector } from 'redux-form'; import { Trans, withNamespaces } from 'react-i18next'; import flow from 'lodash/flow'; +import i18n from '../../../i18n'; import Tabs from '../../ui/Tabs'; import { toggleAllServices } from '../../../helpers/helpers'; -import { renderField, renderRadioField, renderSelectField, renderServiceField, ip, mac, required } from '../../../helpers/form'; -import { CLIENT_ID, SERVICES } from '../../../helpers/constants'; +import { + renderField, + renderGroupField, + renderSelectField, + renderServiceField, +} from '../../../helpers/form'; +import { SERVICES } from '../../../helpers/constants'; import './Service.css'; const settingsCheckboxes = [ @@ -34,6 +40,67 @@ const settingsCheckboxes = [ }, ]; +const validate = (values) => { + const errors = {}; + const { name, ids } = values; + + if (!name || !name.length) { + errors.name = i18n.t('form_error_required'); + } + + if (ids && ids.length) { + const idArrayErrors = []; + ids.forEach((id, idx) => { + if (!id || !id.length) { + idArrayErrors[idx] = i18n.t('form_error_required'); + } + }); + + if (idArrayErrors.length) { + errors.ids = idArrayErrors; + } + } + + return errors; +}; + +const renderFields = (placeholder, buttonTitle) => + function cell(row) { + const { + fields, + meta: { error }, + } = row; + + return ( +
+ {fields.map((ip, index) => ( +
+ fields.remove(index)} + /> +
+ ))} + + {error &&
{error}
} +
+ ); + }; + let Form = (props) => { const { t, @@ -42,92 +109,53 @@ let Form = (props) => { change, pristine, submitting, - clientIdentifier, useGlobalSettings, useGlobalServices, toggleClientModal, processingAdding, processingUpdating, + invalid, } = props; return (
-
-
- - client_identifier - -
- - +
+
+ +
+ +
+
+ + client_identifier + +
+
+ + link + , + ]} + > + client_identifier_desc +
-
-
- {clientIdentifier === CLIENT_ID.IP && ( -
- -
- )} - {clientIdentifier === CLIENT_ID.MAC && ( -
- -
- )} -
-
- -
-
-
- - link - , - ]} - > - client_identifier_desc - + +
+
@@ -140,7 +168,11 @@ let Form = (props) => { type="checkbox" component={renderSelectField} placeholder={t(setting.placeholder)} - disabled={setting.name !== 'use_global_settings' ? useGlobalSettings : false} + disabled={ + setting.name !== 'use_global_settings' + ? useGlobalSettings + : false + } />
))} @@ -210,7 +242,13 @@ let Form = (props) => { @@ -227,22 +265,20 @@ Form.propTypes = { change: PropTypes.func.isRequired, submitting: PropTypes.bool.isRequired, toggleClientModal: PropTypes.func.isRequired, - clientIdentifier: PropTypes.string, useGlobalSettings: PropTypes.bool, useGlobalServices: PropTypes.bool, t: PropTypes.func.isRequired, processingAdding: PropTypes.bool.isRequired, processingUpdating: PropTypes.bool.isRequired, + invalid: PropTypes.bool.isRequired, }; const selector = formValueSelector('clientForm'); Form = connect((state) => { - const clientIdentifier = selector(state, 'identifier'); const useGlobalSettings = selector(state, 'use_global_settings'); const useGlobalServices = selector(state, 'use_global_blocked_services'); return { - clientIdentifier, useGlobalSettings, useGlobalServices, }; @@ -253,5 +289,6 @@ export default flow([ reduxForm({ form: 'clientForm', enableReinitialize: true, + validate, }), ])(Form); diff --git a/client/src/components/ui/Icons.css b/client/src/components/ui/Icons.css index 17d608fd..da2c5f4e 100644 --- a/client/src/components/ui/Icons.css +++ b/client/src/components/ui/Icons.css @@ -3,3 +3,8 @@ vertical-align: middle; height: 100%; } + +.icon--close { + width: 24px; + height: 24px; +} diff --git a/client/src/components/ui/Icons.js b/client/src/components/ui/Icons.js index 100e74de..24d05e65 100644 --- a/client/src/components/ui/Icons.js +++ b/client/src/components/ui/Icons.js @@ -167,6 +167,14 @@ const Icons = () => ( + + + + + + + + ); diff --git a/client/src/helpers/form.js b/client/src/helpers/form.js index fc0286a1..55e0b0a0 100644 --- a/client/src/helpers/form.js +++ b/client/src/helpers/form.js @@ -29,6 +29,50 @@ export const renderField = ({ ); +export const renderGroupField = ({ + input, + id, + className, + placeholder, + type, + disabled, + autoComplete, + isActionAvailable, + removeField, + meta: { touched, error }, +}) => ( + +
+ + {isActionAvailable && + + + + } +
+ + {!disabled && + touched && + (error && {error})} +
+); + export const renderRadioField = ({ input, placeholder, disabled, meta: { touched, error }, }) => ( @@ -102,6 +146,7 @@ export const renderServiceField = ({ ); +// Validation functions export const required = (value) => { if (value || value === 0) { return false; diff --git a/client/src/helpers/formatClientCell.js b/client/src/helpers/formatClientCell.js index 931210a7..30e9e99b 100644 --- a/client/src/helpers/formatClientCell.js +++ b/client/src/helpers/formatClientCell.js @@ -1,5 +1,5 @@ import React, { Fragment } from 'react'; -import { getClientInfo, normalizeWhois } from './helpers'; +import { getClientInfo, getAutoClientInfo, normalizeWhois } from './helpers'; import { WHOIS_ICONS } from './constants'; const getFormattedWhois = (whois, t) => { @@ -23,7 +23,7 @@ const getFormattedWhois = (whois, t) => { }; export const formatClientCell = (value, clients, autoClients, t) => { - const clientInfo = getClientInfo(clients, value) || getClientInfo(autoClients, value); + const clientInfo = getClientInfo(clients, value) || getAutoClientInfo(autoClients, value); const { name, whois } = clientInfo; let whoisContainer = ''; let nameContainer = value; diff --git a/client/src/helpers/helpers.js b/client/src/helpers/helpers.js index ced2e9ad..82389111 100644 --- a/client/src/helpers/helpers.js +++ b/client/src/helpers/helpers.js @@ -248,6 +248,20 @@ export const redirectToCurrentProtocol = (values, httpPort = 80) => { export const normalizeTextarea = text => text && text.replace(/[;, ]/g, '\n').split('\n').filter(n => n); export const getClientInfo = (clients, ip) => { + const client = clients + .find(item => item.ip_addrs && item.ip_addrs.find(clientIp => clientIp === ip)); + + if (!client) { + return ''; + } + + const { name, whois_info } = client; + const whois = Object.keys(whois_info).length > 0 ? whois_info : ''; + + return { name, whois }; +}; + +export const getAutoClientInfo = (clients, ip) => { const client = clients.find(item => ip === item.ip); if (!client) {