diff --git a/client/src/__locales/en.json b/client/src/__locales/en.json index 8a6a9d10..a2633ad8 100644 --- a/client/src/__locales/en.json +++ b/client/src/__locales/en.json @@ -257,12 +257,12 @@ "query_log_cleared": "The query log has been successfully cleared", "query_log_updated": "The query log has been successfully updated", "query_log_clear": "Clear query logs", - "query_log_retention": "Query logs retention", + "query_log_retention": "Query logs rotation", "query_log_enable": "Enable log", "query_log_configuration": "Logs configuration", "query_log_disabled": "The query log is disabled and can be configured in the <0>settings", "query_log_strict_search": "Use double quotes for strict search", - "query_log_retention_confirm": "Are you sure you want to change query log retention? If you decrease the interval value, some data will be lost", + "query_log_retention_confirm": "Are you sure you want to change query log rotation? If you decrease the interval value, some data will be lost", "anonymize_client_ip": "Anonymize client IP", "anonymize_client_ip_desc": "Don't save the client's full IP address to logs or statistics", "dns_config": "DNS server configuration", @@ -669,6 +669,8 @@ "disable_notify_for_hours_plural": "Disable protection for {{count}} hours", "disable_notify_until_tomorrow": "Disable protection until tomorrow", "enable_protection_timer": "Protection will be enabled in {{time}}", + "custom_retention_input": "Enter retention in hours", + "custom_rotation_input": "Enter rotation in hours", "protection_section_label": "Protection", "log_and_stats_section_label": "Query log and statistics", "ignore_query_log": "Ignore this client in query log", diff --git a/client/src/components/Settings/LogsConfig/Form.js b/client/src/components/Settings/LogsConfig/Form.js index b29b974e..dbecc183 100644 --- a/client/src/components/Settings/LogsConfig/Form.js +++ b/client/src/components/Settings/LogsConfig/Form.js @@ -1,25 +1,37 @@ -import React from 'react'; +import React, { useEffect } from 'react'; import PropTypes from 'prop-types'; -import { Field, reduxForm } from 'redux-form'; +import { + change, + Field, + formValueSelector, + reduxForm, +} from 'redux-form'; +import { connect } from 'react-redux'; import { Trans, withTranslation } from 'react-i18next'; import flow from 'lodash/flow'; import { CheckboxField, - renderRadioField, toFloatNumber, - renderTextareaField, + renderTextareaField, renderInputField, renderRadioField, } from '../../../helpers/form'; import { FORM_NAME, QUERY_LOG_INTERVALS_DAYS, HOUR, DAY, + RETENTION_CUSTOM, + RETENTION_CUSTOM_INPUT, + RETENTION_RANGE, + CUSTOM_INTERVAL, } from '../../../helpers/constants'; import '../FormButton.css'; + const getIntervalTitle = (interval, t) => { switch (interval) { + case RETENTION_CUSTOM: + return t('settings_custom'); case 6 * HOUR: return t('interval_6_hour'); case DAY: @@ -42,11 +54,26 @@ const getIntervalFields = (processing, t, toNumber) => QUERY_LOG_INTERVALS_DAYS. /> )); -const Form = (props) => { +let Form = (props) => { const { - handleSubmit, submitting, invalid, processing, processingClear, handleClear, t, + handleSubmit, + submitting, + invalid, + processing, + processingClear, + handleClear, + t, + interval, + customInterval, + dispatch, } = props; + useEffect(() => { + if (QUERY_LOG_INTERVALS_DAYS.includes(interval)) { + dispatch(change(FORM_NAME.LOG_CONFIG, CUSTOM_INTERVAL, null)); + } + }, [interval]); + return (
@@ -73,6 +100,37 @@ const Form = (props) => {
+ + {!QUERY_LOG_INTERVALS_DAYS.includes(interval) && ( +
+
+ {t('custom_rotation_input')} +
+ +
+ )} {getIntervalFields(processing, t, toFloatNumber)}
@@ -96,7 +154,12 @@ const Form = (props) => { @@ -121,8 +184,22 @@ Form.propTypes = { processing: PropTypes.bool.isRequired, processingClear: PropTypes.bool.isRequired, t: PropTypes.func.isRequired, + interval: PropTypes.number, + customInterval: PropTypes.number, + dispatch: PropTypes.func.isRequired, }; +const selector = formValueSelector(FORM_NAME.LOG_CONFIG); + +Form = connect((state) => { + const interval = selector(state, 'interval'); + const customInterval = selector(state, CUSTOM_INTERVAL); + return { + interval, + customInterval, + }; +})(Form); + export default flow([ withTranslation(), reduxForm({ form: FORM_NAME.LOG_CONFIG }), diff --git a/client/src/components/Settings/LogsConfig/index.js b/client/src/components/Settings/LogsConfig/index.js index 146a77b0..3e609a2d 100644 --- a/client/src/components/Settings/LogsConfig/index.js +++ b/client/src/components/Settings/LogsConfig/index.js @@ -4,15 +4,22 @@ import { withTranslation } from 'react-i18next'; import Card from '../../ui/Card'; import Form from './Form'; +import { HOUR } from '../../../helpers/constants'; class LogsConfig extends Component { handleFormSubmit = (values) => { const { t, interval: prevInterval } = this.props; - const { interval } = values; + const { interval, customInterval, ...rest } = values; - const data = { ...values, ignored: values.ignored ? values.ignored.split('\n') : [] }; + const newInterval = customInterval ? customInterval * HOUR : interval; - if (interval !== prevInterval) { + const data = { + ...rest, + ignored: values.ignored ? values.ignored.split('\n') : [], + interval: newInterval, + }; + + if (newInterval < prevInterval) { // eslint-disable-next-line no-alert if (window.confirm(t('query_log_retention_confirm'))) { this.props.setLogsConfig(data); @@ -32,7 +39,14 @@ class LogsConfig extends Component { render() { const { - t, enabled, interval, processing, processingClear, anonymize_client_ip, ignored, + t, + enabled, + interval, + processing, + processingClear, + anonymize_client_ip, + ignored, + customInterval, } = this.props; return ( @@ -46,6 +60,7 @@ class LogsConfig extends Component { initialValues={{ enabled, interval, + customInterval, anonymize_client_ip, ignored: ignored.join('\n'), }} @@ -62,6 +77,7 @@ class LogsConfig extends Component { LogsConfig.propTypes = { interval: PropTypes.number.isRequired, + customInterval: PropTypes.number, enabled: PropTypes.bool.isRequired, anonymize_client_ip: PropTypes.bool.isRequired, processing: PropTypes.bool.isRequired, diff --git a/client/src/components/Settings/Settings.css b/client/src/components/Settings/Settings.css index b6903427..25532b45 100644 --- a/client/src/components/Settings/Settings.css +++ b/client/src/components/Settings/Settings.css @@ -18,6 +18,11 @@ font-size: 14px; } +.form__group--input { + max-width: 300px; + margin: 0 1.5rem 10px; +} + .form__group--checkbox { margin-bottom: 25px; } diff --git a/client/src/components/Settings/StatsConfig/Form.js b/client/src/components/Settings/StatsConfig/Form.js index e9cd02fd..087e9578 100644 --- a/client/src/components/Settings/StatsConfig/Form.js +++ b/client/src/components/Settings/StatsConfig/Form.js @@ -1,32 +1,44 @@ -import React from 'react'; +import React, { useEffect } from 'react'; import PropTypes from 'prop-types'; -import { Field, reduxForm } from 'redux-form'; +import { + change, Field, formValueSelector, reduxForm, +} from 'redux-form'; import { Trans, withTranslation } from 'react-i18next'; import flow from 'lodash/flow'; +import { connect } from 'react-redux'; + import { renderRadioField, toNumber, CheckboxField, renderTextareaField, + toFloatNumber, + renderInputField, } from '../../../helpers/form'; import { FORM_NAME, STATS_INTERVALS_DAYS, DAY, + RETENTION_CUSTOM, + RETENTION_CUSTOM_INPUT, + CUSTOM_INTERVAL, + RETENTION_RANGE, } from '../../../helpers/constants'; import '../FormButton.css'; const getIntervalTitle = (intervalMs, t) => { - switch (intervalMs / DAY) { - case 1: + switch (intervalMs) { + case RETENTION_CUSTOM: + return t('settings_custom'); + case DAY: return t('interval_24_hour'); default: return t('interval_days', { count: intervalMs / DAY }); } }; -const Form = (props) => { +let Form = (props) => { const { handleSubmit, processing, @@ -35,8 +47,17 @@ const Form = (props) => { handleReset, processingReset, t, + interval, + customInterval, + dispatch, } = props; + useEffect(() => { + if (STATS_INTERVALS_DAYS.includes(interval)) { + dispatch(change(FORM_NAME.STATS_CONFIG, CUSTOM_INTERVAL, null)); + } + }, [interval]); + return (
@@ -56,6 +77,37 @@ const Form = (props) => {
+ + {!STATS_INTERVALS_DAYS.includes(interval) && ( +
+
+ {t('custom_retention_input')} +
+ +
+ )} {STATS_INTERVALS_DAYS.map((interval) => ( { @@ -116,8 +173,22 @@ Form.propTypes = { processing: PropTypes.bool.isRequired, processingReset: PropTypes.bool.isRequired, t: PropTypes.func.isRequired, + interval: PropTypes.number, + customInterval: PropTypes.number, + dispatch: PropTypes.func.isRequired, }; +const selector = formValueSelector(FORM_NAME.STATS_CONFIG); + +Form = connect((state) => { + const interval = selector(state, 'interval'); + const customInterval = selector(state, CUSTOM_INTERVAL); + return { + interval, + customInterval, + }; +})(Form); + export default flow([ withTranslation(), reduxForm({ form: FORM_NAME.STATS_CONFIG }), diff --git a/client/src/components/Settings/StatsConfig/index.js b/client/src/components/Settings/StatsConfig/index.js index 83807afa..7b68064c 100644 --- a/client/src/components/Settings/StatsConfig/index.js +++ b/client/src/components/Settings/StatsConfig/index.js @@ -4,13 +4,18 @@ import { withTranslation } from 'react-i18next'; import Card from '../../ui/Card'; import Form from './Form'; +import { HOUR } from '../../../helpers/constants'; class StatsConfig extends Component { - handleFormSubmit = ({ enabled, interval, ignored }) => { + handleFormSubmit = ({ + enabled, interval, ignored, customInterval, + }) => { const { t, interval: prevInterval } = this.props; + const newInterval = customInterval ? customInterval * HOUR : interval; + const config = { enabled, - interval, + interval: newInterval, ignored: ignored ? ignored.split('\n') : [], }; @@ -33,7 +38,13 @@ class StatsConfig extends Component { render() { const { - t, interval, processing, processingReset, ignored, enabled, + t, + interval, + customInterval, + processing, + processingReset, + ignored, + enabled, } = this.props; return ( @@ -46,6 +57,7 @@ class StatsConfig extends Component { ({ ...state, ...payload, + customInterval: !QUERY_LOG_INTERVALS_DAYS.includes(payload.interval) + ? payload.interval / HOUR + : null, processingGetConfig: false, }), @@ -95,6 +100,7 @@ const queryLogs = handleActions( anonymize_client_ip: false, isDetailed: true, isEntireLog: false, + customInterval: null, }, ); diff --git a/client/src/reducers/stats.js b/client/src/reducers/stats.js index 2e5a7e48..a1c63e14 100644 --- a/client/src/reducers/stats.js +++ b/client/src/reducers/stats.js @@ -1,6 +1,6 @@ import { handleActions } from 'redux-actions'; import { normalizeTopClients } from '../helpers/helpers'; -import { DAY } from '../helpers/constants'; +import { DAY, HOUR, STATS_INTERVALS_DAYS } from '../helpers/constants'; import * as actions from '../actions/stats'; @@ -27,6 +27,9 @@ const stats = handleActions( [actions.getStatsConfigSuccess]: (state, { payload }) => ({ ...state, ...payload, + customInterval: !STATS_INTERVALS_DAYS.includes(payload.interval) + ? payload.interval / HOUR + : null, processingGetConfig: false, }), @@ -93,6 +96,7 @@ const stats = handleActions( processingStats: true, processingReset: false, interval: DAY, + customInterval: null, ...defaultStats, }, );