diff --git a/client/src/__locales/en.json b/client/src/__locales/en.json index 03a7741c..09f94851 100644 --- a/client/src/__locales/en.json +++ b/client/src/__locales/en.json @@ -361,5 +361,10 @@ "encryption_certificates_source_path": "Set a certificates file path", "encryption_certificates_source_content":"Paste the certificates contents", "encryption_key_source_path": "Set a private key file", - "encryption_key_source_content": "Paste the private key contents" + "encryption_key_source_content": "Paste the private key contents", + "stats_params": "Statistics configuration", + "config_successfully_saved": "Configuration successfully saved", + "interval_24_hour": "24 hours", + "interval_days": "{{value}} days", + "time_period": "Time period" } diff --git a/client/src/actions/stats.js b/client/src/actions/stats.js new file mode 100644 index 00000000..19175817 --- /dev/null +++ b/client/src/actions/stats.js @@ -0,0 +1,36 @@ +import { createAction } from 'redux-actions'; +import Api from '../api/Api'; +import { addErrorToast, addSuccessToast } from './index'; + +const apiClient = new Api(); + +export const getStatsConfigRequest = createAction('GET_LOGS_CONFIG_REQUEST'); +export const getStatsConfigFailure = createAction('GET_LOGS_CONFIG_FAILURE'); +export const getStatsConfigSuccess = createAction('GET_LOGS_CONFIG_SUCCESS'); + +export const getStatsConfig = () => async (dispatch) => { + dispatch(getStatsConfigRequest()); + try { + const data = await apiClient.getStatsInfo(); + dispatch(getStatsConfigSuccess(data)); + } catch (error) { + dispatch(addErrorToast({ error })); + dispatch(getStatsConfigFailure()); + } +}; + +export const setStatsConfigRequest = createAction('SET_STATS_CONFIG_REQUEST'); +export const setStatsConfigFailure = createAction('SET_STATS_CONFIG_FAILURE'); +export const setStatsConfigSuccess = createAction('SET_STATS_CONFIG_SUCCESS'); + +export const setStatsConfig = config => async (dispatch) => { + dispatch(setStatsConfigRequest()); + try { + await apiClient.setStatsConfig(config); + dispatch(addSuccessToast('config_successfully_saved')); + dispatch(setStatsConfigSuccess(config)); + } catch (error) { + dispatch(addErrorToast({ error })); + dispatch(setStatsConfigFailure()); + } +}; diff --git a/client/src/api/Api.js b/client/src/api/Api.js index a857766c..2967ec53 100644 --- a/client/src/api/Api.js +++ b/client/src/api/Api.js @@ -527,4 +527,22 @@ export default class Api { }; return this.makeRequest(path, method, parameters); } + + // Settings for statistics + STATS_INFO = { path: 'stats_info', method: 'GET' }; + STATS_CONFIG = { path: 'stats_config', method: 'POST' }; + + getStatsInfo() { + const { path, method } = this.STATS_INFO; + return this.makeRequest(path, method); + } + + setStatsConfig(data) { + const { path, method } = this.STATS_CONFIG; + const config = { + data, + headers: { 'Content-Type': 'application/json' }, + }; + return this.makeRequest(path, method, config); + } } diff --git a/client/src/components/Settings/Settings.css b/client/src/components/Settings/Settings.css index 7f12dbbe..0cc18f6e 100644 --- a/client/src/components/Settings/Settings.css +++ b/client/src/components/Settings/Settings.css @@ -104,3 +104,8 @@ min-width: 23px; padding: 5px; } + +.custom-control-label, +.custom-control-label:before { + transition: 0.3s ease-in-out background-color, 0.3s ease-in-out color; +} diff --git a/client/src/components/Settings/StatsConfig/Form.js b/client/src/components/Settings/StatsConfig/Form.js new file mode 100644 index 00000000..f1c3df24 --- /dev/null +++ b/client/src/components/Settings/StatsConfig/Form.js @@ -0,0 +1,71 @@ +import React from 'react'; +import PropTypes from 'prop-types'; +import { Field, reduxForm } from 'redux-form'; +import { Trans, withNamespaces } from 'react-i18next'; +import flow from 'lodash/flow'; + +import { renderRadioField, toNumber } from '../../../helpers/form'; +import { STATS_INTERVALS } from '../../../helpers/constants'; + +const getIntervalFields = (processing, t, handleChange, toNumber) => + STATS_INTERVALS.map((interval) => { + const title = interval === 1 + ? t('interval_24_hour') + : t('interval_days', { value: interval }); + + return ( + + ); + }); + +const Form = (props) => { + const { + handleSubmit, handleChange, processing, t, + } = props; + + return ( +
+
+
+ +
+
+
+
+ {getIntervalFields(processing, t, handleChange, toNumber)} +
+
+
+
+
+ ); +}; + +Form.propTypes = { + handleSubmit: PropTypes.func.isRequired, + handleChange: PropTypes.func, + change: PropTypes.func.isRequired, + submitting: PropTypes.bool.isRequired, + invalid: PropTypes.bool.isRequired, + processing: PropTypes.bool.isRequired, + t: PropTypes.func.isRequired, +}; + +export default flow([ + withNamespaces(), + reduxForm({ + form: 'logConfigForm', + }), +])(Form); diff --git a/client/src/components/Settings/StatsConfig/index.js b/client/src/components/Settings/StatsConfig/index.js new file mode 100644 index 00000000..5513e7d6 --- /dev/null +++ b/client/src/components/Settings/StatsConfig/index.js @@ -0,0 +1,49 @@ +import React, { Component } from 'react'; +import PropTypes from 'prop-types'; +import { withNamespaces } from 'react-i18next'; +import debounce from 'lodash/debounce'; + +import { DEBOUNCE_TIMEOUT } from '../../../helpers/constants'; +import Form from './Form'; +import Card from '../../ui/Card'; + +class StatsConfig extends Component { + handleFormChange = debounce((values) => { + this.props.setStatsConfig(values); + }, DEBOUNCE_TIMEOUT); + + render() { + const { + t, + interval, + processing, + } = this.props; + + return ( + +
+
+
+
+ ); + } +} + +StatsConfig.propTypes = { + interval: PropTypes.number.isRequired, + processing: PropTypes.bool.isRequired, + setStatsConfig: PropTypes.func.isRequired, + t: PropTypes.func.isRequired, +}; + +export default withNamespaces()(StatsConfig); diff --git a/client/src/components/Settings/index.js b/client/src/components/Settings/index.js index 7391cbaf..7d703cb8 100644 --- a/client/src/components/Settings/index.js +++ b/client/src/components/Settings/index.js @@ -3,6 +3,7 @@ import PropTypes from 'prop-types'; import { withNamespaces, Trans } from 'react-i18next'; import Services from './Services'; +import StatsConfig from './StatsConfig'; import Checkbox from '../ui/Checkbox'; import Loading from '../ui/Loading'; import PageTitle from '../ui/PageTitle'; @@ -37,6 +38,7 @@ class Settings extends Component { componentDidMount() { this.props.initSettings(this.settings); this.props.getBlockedServices(); + this.props.getStatsConfig(); } renderSettings = (settings) => { @@ -62,7 +64,12 @@ class Settings extends Component { render() { const { - settings, services, setBlockedServices, t, + settings, + services, + setBlockedServices, + setStatsConfig, + stats, + t, } = this.props; return ( @@ -78,6 +85,13 @@ class Settings extends Component { +
+ +
{ - const { settings, services } = state; + const { settings, services, stats } = state; const props = { settings, services, + stats, }; return props; }; @@ -17,6 +19,8 @@ const mapDispatchToProps = { toggleSetting, getBlockedServices, setBlockedServices, + getStatsConfig, + setStatsConfig, }; export default connect( diff --git a/client/src/helpers/constants.js b/client/src/helpers/constants.js index 74ac19c1..b2ec30c2 100644 --- a/client/src/helpers/constants.js +++ b/client/src/helpers/constants.js @@ -260,3 +260,5 @@ export const FILTERED_STATUS = { FILTERED_BLOCKED_SERVICE: 'FilteredBlockedService', REWRITE: 'Rewrite', }; + +export const STATS_INTERVALS = [1, 7, 30, 90]; diff --git a/client/src/reducers/index.js b/client/src/reducers/index.js index 2913f5cc..ef197cb8 100644 --- a/client/src/reducers/index.js +++ b/client/src/reducers/index.js @@ -11,6 +11,7 @@ import clients from './clients'; import access from './access'; import rewrites from './rewrites'; import services from './services'; +import stats from './stats'; const settings = handleActions({ [actions.initSettingsRequest]: state => ({ ...state, processing: true }), @@ -218,6 +219,14 @@ const dashboard = handleActions({ clients: [], autoClients: [], topStats: [], + stats: { + dns_queries: '', + blocked_filtering: '', + replaced_safebrowsing: '', + replaced_parental: '', + replaced_safesearch: '', + avg_processing_time: '', + }, }); const queryLogs = handleActions({ @@ -230,7 +239,11 @@ const queryLogs = handleActions({ [actions.downloadQueryLogRequest]: state => ({ ...state, logsDownloading: true }), [actions.downloadQueryLogFailure]: state => ({ ...state, logsDownloading: false }), [actions.downloadQueryLogSuccess]: state => ({ ...state, logsDownloading: false }), -}, { getLogsProcessing: false, logsDownloading: false }); +}, { + getLogsProcessing: false, + logsDownloading: false, + logs: [], +}); const filtering = handleActions({ [actions.setRulesRequest]: state => ({ ...state, processingRules: true }), @@ -426,6 +439,7 @@ export default combineReducers({ access, rewrites, services, + stats, loadingBar: loadingBarReducer, form: formReducer, }); diff --git a/client/src/reducers/stats.js b/client/src/reducers/stats.js new file mode 100644 index 00000000..48e07bb5 --- /dev/null +++ b/client/src/reducers/stats.js @@ -0,0 +1,27 @@ +import { handleActions } from 'redux-actions'; + +import * as actions from '../actions/stats'; + +const stats = handleActions({ + [actions.getStatsConfigRequest]: state => ({ ...state, getConfigProcessing: true }), + [actions.getStatsConfigFailure]: state => ({ ...state, getConfigProcessing: false }), + [actions.getStatsConfigSuccess]: (state, { payload }) => ({ + ...state, + interval: payload.interval, + getConfigProcessing: false, + }), + + [actions.setStatsConfigRequest]: state => ({ ...state, setConfigProcessing: true }), + [actions.setStatsConfigFailure]: state => ({ ...state, setConfigProcessing: false }), + [actions.setStatsConfigSuccess]: (state, { payload }) => ({ + ...state, + interval: payload.interval, + setConfigProcessing: false, + }), +}, { + getConfigProcessing: false, + setConfigProcessing: false, + interval: 1, +}); + +export default stats;