From 2e493e0226d3f22941fd09eed44ebb67a4d2874a Mon Sep 17 00:00:00 2001 From: Artem Baskal Date: Thu, 12 Dec 2019 21:48:17 +0300 Subject: [PATCH] + client: add clients forms validation and cache findClients function --- client/src/__locales/en.json | 1 + client/src/actions/queryLogs.js | 28 ++-- client/src/actions/stats.js | 48 +++--- client/src/components/Logs/Filters/Form.js | 4 +- .../src/components/Settings/Clients/Form.js | 30 ++-- client/src/components/Settings/Dhcp/Form.js | 16 +- .../Settings/Dhcp/StaticLeases/Form.js | 8 +- .../components/Settings/Dns/Access/Form.js | 8 +- .../components/Settings/Dns/Rewrites/Form.js | 6 +- .../components/Settings/Encryption/Form.js | 12 +- client/src/helpers/constants.js | 7 +- client/src/helpers/form.js | 141 ++++++++++-------- client/src/install/Setup/Auth.js | 8 +- client/src/install/Setup/Settings.js | 6 +- client/src/install/Setup/renderField.js | 19 --- client/src/login/Login/Form.js | 7 +- 16 files changed, 187 insertions(+), 162 deletions(-) delete mode 100644 client/src/install/Setup/renderField.js diff --git a/client/src/__locales/en.json b/client/src/__locales/en.json index 05794bcb..926ee7a5 100644 --- a/client/src/__locales/en.json +++ b/client/src/__locales/en.json @@ -23,6 +23,7 @@ "form_error_ip6_format": "Invalid IPv6 format", "form_error_ip_format": "Invalid IP format", "form_error_mac_format": "Invalid MAC format", + "form_error_client_id_format": "Invalid client ID format", "form_error_positive": "Must be greater than 0", "form_error_negative": "Must be equal to 0 or greater", "dhcp_form_gateway_input": "Gateway IP", diff --git a/client/src/actions/queryLogs.js b/client/src/actions/queryLogs.js index 155f0a76..84e5f992 100644 --- a/client/src/actions/queryLogs.js +++ b/client/src/actions/queryLogs.js @@ -5,20 +5,28 @@ import { addErrorToast, addSuccessToast } from './index'; import { normalizeLogs, getParamsForClientsSearch, addClientInfo } from '../helpers/helpers'; import { TABLE_DEFAULT_PAGE_SIZE } from '../helpers/constants'; -const getLogsWithParams = async (config) => { - const { older_than, filter, ...values } = config; - const rawLogs = await apiClient.getQueryLog({ ...filter, older_than }); - const { data, oldest } = rawLogs; - const logs = normalizeLogs(data); - const clientsParams = getParamsForClientsSearch(logs, 'client'); - const clients = await apiClient.findClients(clientsParams); - const logsWithClientInfo = addClientInfo(logs, clients, 'client'); +// Cache clients in closure +const getLogsWithParamsWrapper = () => { + let clients = {}; + return async (config) => { + const { older_than, filter, ...values } = config; + const rawLogs = await apiClient.getQueryLog({ ...filter, older_than }); + const { data, oldest } = rawLogs; + const logs = normalizeLogs(data); + const clientsParams = getParamsForClientsSearch(logs, 'client'); + if (!Object.values(clientsParams).every(client => client in clients)) { + clients = await apiClient.findClients(clientsParams); + } + const logsWithClientInfo = addClientInfo(logs, clients, 'client'); - return { - logs: logsWithClientInfo, oldest, older_than, filter, ...values, + return { + logs: logsWithClientInfo, oldest, older_than, filter, ...values, + }; }; }; +const getLogsWithParams = getLogsWithParamsWrapper(); + export const getAdditionalLogsRequest = createAction('GET_ADDITIONAL_LOGS_REQUEST'); export const getAdditionalLogsFailure = createAction('GET_ADDITIONAL_LOGS_FAILURE'); export const getAdditionalLogsSuccess = createAction('GET_ADDITIONAL_LOGS_SUCCESS'); diff --git a/client/src/actions/stats.js b/client/src/actions/stats.js index 25897aab..b928b85a 100644 --- a/client/src/actions/stats.js +++ b/client/src/actions/stats.js @@ -39,30 +39,38 @@ export const getStatsRequest = createAction('GET_STATS_REQUEST'); export const getStatsFailure = createAction('GET_STATS_FAILURE'); export const getStatsSuccess = createAction('GET_STATS_SUCCESS'); -export const getStats = () => async (dispatch) => { - dispatch(getStatsRequest()); - try { - const stats = await apiClient.getStats(); - const normalizedTopClients = normalizeTopStats(stats.top_clients); - const clientsParams = getParamsForClientsSearch(normalizedTopClients, 'name'); - const clients = await apiClient.findClients(clientsParams); - const topClientsWithInfo = addClientInfo(normalizedTopClients, clients, 'name'); +// Cache clients in closure +const getStatsWrapper = () => { + let clients = {}; + return () => async (dispatch) => { + dispatch(getStatsRequest()); + try { + const stats = await apiClient.getStats(); + const normalizedTopClients = normalizeTopStats(stats.top_clients); + const clientsParams = getParamsForClientsSearch(normalizedTopClients, 'name'); + if (!Object.values(clientsParams).every(client => client in clients)) { + clients = await apiClient.findClients(clientsParams); + } + const topClientsWithInfo = addClientInfo(normalizedTopClients, clients, 'name'); - const normalizedStats = { - ...stats, - top_blocked_domains: normalizeTopStats(stats.top_blocked_domains), - top_clients: topClientsWithInfo, - top_queried_domains: normalizeTopStats(stats.top_queried_domains), - avg_processing_time: secondsToMilliseconds(stats.avg_processing_time), - }; + const normalizedStats = { + ...stats, + top_blocked_domains: normalizeTopStats(stats.top_blocked_domains), + top_clients: topClientsWithInfo, + top_queried_domains: normalizeTopStats(stats.top_queried_domains), + avg_processing_time: secondsToMilliseconds(stats.avg_processing_time), + }; - dispatch(getStatsSuccess(normalizedStats)); - } catch (error) { - dispatch(addErrorToast({ error })); - dispatch(getStatsFailure()); - } + dispatch(getStatsSuccess(normalizedStats)); + } catch (error) { + dispatch(addErrorToast({ error })); + dispatch(getStatsFailure()); + } + }; }; +export const getStats = getStatsWrapper(); + export const resetStatsRequest = createAction('RESET_STATS_REQUEST'); export const resetStatsFailure = createAction('RESET_STATS_FAILURE'); export const resetStatsSuccess = createAction('RESET_STATS_SUCCESS'); diff --git a/client/src/components/Logs/Filters/Form.js b/client/src/components/Logs/Filters/Form.js index cedc6f5e..9e4f4365 100644 --- a/client/src/components/Logs/Filters/Form.js +++ b/client/src/components/Logs/Filters/Form.js @@ -4,7 +4,7 @@ import { Field, reduxForm } from 'redux-form'; import { withNamespaces, Trans } from 'react-i18next'; import flow from 'lodash/flow'; -import { renderField } from '../../../helpers/form'; +import { renderInputField } from '../../../helpers/form'; import { RESPONSE_FILTER } from '../../../helpers/constants'; import Tooltip from '../../ui/Tooltip'; @@ -65,7 +65,7 @@ const Form = (props) => { { const errors = {}; const { name, ids } = values; - - if (!name || !name.length) { - errors.name = i18n.t('form_error_required'); - } + errors.name = required(name); if (ids && ids.length) { const idArrayErrors = []; ids.forEach((id, idx) => { - if (!id || !id.length) { - idArrayErrors[idx] = i18n.t('form_error_required'); - } + idArrayErrors[idx] = required(id) || clientId(id); }); if (idArrayErrors.length) { errors.ids = idArrayErrors; } } - return errors; }; -const renderFields = (placeholder, buttonTitle) => + +const renderFieldsWrapper = (placeholder, buttonTitle) => function cell(row) { const { fields, - meta: { error }, } = row; - return (
{fields.map((ip, index) => ( @@ -84,6 +78,7 @@ const renderFields = (placeholder, buttonTitle) => placeholder={placeholder} isActionAvailable={index !== 0} removeField={() => fields.remove(index)} + normalize={data => data && data.trim()} />
))} @@ -97,11 +92,13 @@ const renderFields = (placeholder, buttonTitle) => - {error &&
{error}
} ); }; +// Should create function outside of component to prevent component re-renders +const renderFields = renderFieldsWrapper(i18n.t('form_enter_id'), i18n.t('form_add_id')); + let Form = (props) => { const { t, @@ -126,10 +123,11 @@ let Form = (props) => { data && data.trim()} /> @@ -155,7 +153,7 @@ let Form = (props) => {
diff --git a/client/src/components/Settings/Dhcp/Form.js b/client/src/components/Settings/Dhcp/Form.js index 8bbef865..619b3f95 100644 --- a/client/src/components/Settings/Dhcp/Form.js +++ b/client/src/components/Settings/Dhcp/Form.js @@ -5,7 +5,7 @@ import { Field, reduxForm, formValueSelector } from 'redux-form'; import { Trans, withNamespaces } from 'react-i18next'; import flow from 'lodash/flow'; -import { renderField, required, ipv4, isPositive, toNumber } from '../../../helpers/form'; +import { renderInputField, required, ipv4, isPositive, toNumber } from '../../../helpers/form'; const renderInterfaces = (interfaces => ( Object.keys(interfaces).map((item) => { @@ -116,8 +116,9 @@ let Form = (props) => {
{
{
{
{ { const { @@ -24,7 +24,7 @@ const Form = (props) => { { { { const { @@ -21,7 +22,7 @@ const Form = (props) => { { { { const { @@ -24,7 +24,7 @@ const Form = (props) => { { { { { { { { + const { + input, id, className, placeholder, type, disabled, + autoComplete, meta: { touched, error }, + } = props; -export const renderField = ({ - input, - id, - className, - placeholder, - type, - disabled, - autoComplete, - meta: { touched, error }, -}) => ( - - - {!disabled && - touched && - (error && {error})} - -); + const element = React.createElement(elementType, { + ...input, + id, + className, + placeholder, + autoComplete, + disabled, + type, + }); + return ( + + {element} + {!disabled && touched && (error && {error})} + + ); +}; + +renderField.propTypes = { + id: PropTypes.string.isRequired, + input: PropTypes.object.isRequired, + meta: PropTypes.object.isRequired, + className: PropTypes.string, + placeholder: PropTypes.string, + type: PropTypes.string, + disabled: PropTypes.bool, + autoComplete: PropTypes.bool, +}; + +export const renderTextareaField = props => renderField(props, 'textarea'); + +export const renderInputField = props => renderField(props, 'input'); export const renderGroupField = ({ input, @@ -53,7 +65,7 @@ export const renderGroupField = ({ autoComplete={autoComplete} /> {isActionAvailable && - +
- {!disabled && - touched && - (error && {error})} + touched && + (error && {error})} ); @@ -82,8 +93,8 @@ export const renderRadioField = ({ {placeholder} {!disabled && - touched && - (error && {error})} + touched && + (error && {error})} ); @@ -112,8 +123,8 @@ export const renderSelectField = ({ {!disabled && - touched && - (error && {error})} + touched && + (error && {error})} ); @@ -141,52 +152,67 @@ export const renderServiceField = ({ {!disabled && - touched && - (error && {error})} + touched && + (error && {error})} ); // Validation functions +// If the value is valid, the validation function should return undefined. +// https://redux-form.com/6.6.3/examples/fieldlevelvalidation/ export const required = (value) => { - if (value || value === 0) { - return false; + const formattedValue = typeof value === 'string' ? value.trim() : value; + if (formattedValue || formattedValue === 0 || (formattedValue && formattedValue.length !== 0)) { + return undefined; } return form_error_required; }; export const ipv4 = (value) => { - if (value && !new RegExp(R_IPV4).test(value)) { + if (value && !R_IPV4.test(value)) { return form_error_ip4_format; } - return false; + return undefined; +}; + +export const clientId = (value) => { + if (!value) { + return undefined; + } + const formattedValue = value ? value.trim() : value; + if (formattedValue && !(R_IPV4.test(formattedValue) || R_IPV6.test(formattedValue) + || R_MAC.test(formattedValue) || R_CIDR.test(formattedValue))) { + return form_error_client_id_format; + } + return undefined; }; export const ipv6 = (value) => { - if (value && !new RegExp(R_IPV6).test(value)) { + if (value && !R_IPV6.test(value)) { return form_error_ip6_format; } - return false; + return undefined; }; export const ip = (value) => { - if (value && !new RegExp(R_IPV4).test(value) && !new RegExp(R_IPV6).test(value)) { + if (value && !R_IPV4.test(value) && !R_IPV6.test(value)) { return form_error_ip_format; } - return false; + return undefined; }; export const mac = (value) => { - if (value && !new RegExp(R_MAC).test(value)) { + if (value && !R_MAC.test(value)) { return form_error_mac_format; } - return false; + return undefined; }; export const isPositive = (value) => { if ((value || value === 0) && value <= 0) { return form_error_positive; } - return false; + return undefined; }; export const biggerOrEqualZero = (value) => { @@ -200,42 +226,37 @@ export const port = (value) => { if ((value || value === 0) && (value < 80 || value > 65535)) { return form_error_port_range; } - return false; + return undefined; }; export const portTLS = (value) => { if (value === 0) { - return false; + return undefined; } else if (value && (value < 80 || value > 65535)) { return form_error_port_range; } - return false; + return undefined; }; export const isSafePort = (value) => { if (UNSAFE_PORTS.includes(value)) { return form_error_port_unsafe; } - return false; + return undefined; }; export const domain = (value) => { - if (value && !new RegExp(R_HOST).test(value)) { + if (value && !R_HOST.test(value)) { return form_error_domain_format; } - return false; + return undefined; }; export const answer = (value) => { - if ( - value && - (!new RegExp(R_IPV4).test(value) && - !new RegExp(R_IPV6).test(value) && - !new RegExp(R_HOST).test(value)) - ) { + if (value && (!R_IPV4.test(value) && !R_IPV6.test(value) && !R_HOST.test(value))) { return form_error_answer_format; } - return false; + return undefined; }; export const toNumber = value => value && parseInt(value, 10); diff --git a/client/src/install/Setup/Auth.js b/client/src/install/Setup/Auth.js index d8234d94..d055d17b 100644 --- a/client/src/install/Setup/Auth.js +++ b/client/src/install/Setup/Auth.js @@ -6,7 +6,7 @@ import flow from 'lodash/flow'; import i18n from '../../i18n'; import Controls from './Controls'; -import renderField from './renderField'; +import { renderInputField } from '../../helpers/form'; const required = (value) => { if (value || value === 0) { @@ -48,7 +48,7 @@ const Auth = (props) => { { { { if (value || value === 0) { @@ -133,7 +133,7 @@ class Settings extends Component { ( - - - {!disabled && touched && (error && {error})} - -); - -export default renderField; diff --git a/client/src/login/Login/Form.js b/client/src/login/Login/Form.js index 0524129c..45274b55 100644 --- a/client/src/login/Login/Form.js +++ b/client/src/login/Login/Form.js @@ -4,7 +4,7 @@ import { Field, reduxForm } from 'redux-form'; import { Trans, withNamespaces } from 'react-i18next'; import flow from 'lodash/flow'; -import { renderField, required } from '../../helpers/form'; +import { renderInputField, required } from '../../helpers/form'; const Form = (props) => { const { @@ -19,10 +19,11 @@ const Form = (props) => { username_label { name="password" type="password" className="form-control" - component={renderField} + component={renderInputField} placeholder={t('password_placeholder')} autoComplete="current-password" disabled={processing}