From d0fc1dc54dfbc017f28c6c0afa4623c6259af557 Mon Sep 17 00:00:00 2001 From: Ildar Kamalov <i.kamalov@adguard.com> Date: Thu, 12 Sep 2019 16:19:35 +0300 Subject: [PATCH] + client: handle filters configuration --- client/src/__locales/en.json | 8 +- client/src/__locales/ru.json | 20 +- client/src/actions/filtering.js | 145 ++++++++++++ client/src/actions/index.js | 158 +------------ client/src/api/Api.js | 49 ++-- client/src/components/Filters/Modal.js | 113 ++++------ client/src/components/Filters/UserRules.js | 12 +- client/src/components/Filters/index.js | 212 +++++++++++------- client/src/components/Logs/Logs.css | 4 + client/src/components/Logs/index.js | 6 +- .../components/Settings/FiltersConfig/Form.js | 88 ++++++++ .../Settings/FiltersConfig/index.js | 36 +++ client/src/components/Settings/Settings.css | 19 ++ client/src/components/Settings/index.js | 25 ++- client/src/components/ui/CellWrap.js | 19 ++ client/src/components/ui/Checkbox.css | 9 +- client/src/components/ui/Checkbox.js | 2 +- client/src/containers/Filters.js | 24 +- client/src/containers/Logs.js | 3 +- client/src/containers/Settings.js | 6 +- client/src/helpers/constants.js | 2 + client/src/helpers/form.js | 37 ++- client/src/helpers/helpers.js | 58 ++++- client/src/reducers/filtering.js | 86 +++++++ client/src/reducers/index.js | 64 +----- 25 files changed, 745 insertions(+), 460 deletions(-) create mode 100644 client/src/actions/filtering.js create mode 100644 client/src/components/Settings/FiltersConfig/Form.js create mode 100644 client/src/components/Settings/FiltersConfig/index.js create mode 100644 client/src/components/ui/CellWrap.js create mode 100644 client/src/reducers/filtering.js diff --git a/client/src/__locales/en.json b/client/src/__locales/en.json index 570d28e1..9c59c654 100644 --- a/client/src/__locales/en.json +++ b/client/src/__locales/en.json @@ -378,5 +378,11 @@ "statistics_retention_desc": "If you decrease the interval value, some data will be lost", "statistics_clear": " Clear statistics", "statistics_clear_confirm": "Are you sure you want to clear statistics?", - "statistics_cleared": "Statistics successfully cleared" + "statistics_cleared": "Statistics successfully cleared", + "interval_hours": "{{count}} hour", + "interval_hours_plural": "{{count}} hours", + "filters_configuration": "Filters configuration", + "filters_enable": "Enable filters", + "filters_interval": "Filters update interval", + "disabled": "Disabled" } diff --git a/client/src/__locales/ru.json b/client/src/__locales/ru.json index 4bca94d4..88799f70 100644 --- a/client/src/__locales/ru.json +++ b/client/src/__locales/ru.json @@ -1,10 +1,9 @@ { "client_settings": "Настройки клиентов", "example_upstream_reserved": "вы можете указать DNS-сервер <0>для конкретного домена(ов)</0>", - "upstream_parallel": "Использовать одновременные запроссы ко всем серверам для ускорения обработки запроса", + "upstream_parallel": "Использовать одновременные запросы ко всем серверам для ускорения обработки запроса", "bootstrap_dns": "Bootstrap DNS-серверы", "bootstrap_dns_desc": "Bootstrap DNS-серверы используются для поиска IP-адресов DoH/DoT серверов, которые вы указали.", - "url_added_successfully": "URL успешно добавлен", "check_dhcp_servers": "Проверить DHCP-серверы", "save_config": "Сохранить конфигурацию", "enabled_dhcp": "DHCP-сервер включен", @@ -67,7 +66,6 @@ "disabled_protection": "Защита выкл.", "refresh_statics": "Обновить статистику", "dns_query": "DNS-запросы", - "blocked_by": "Заблокировано фильтрами", "stats_malware_phishing": "Заблокированные вредоносные и фишинговые сайты", "stats_adult": "Заблокированные \"взрослые\" сайты", "stats_query_domain": "Часто запрашиваемые домены", @@ -78,7 +76,6 @@ "top_clients": "Частые клиенты", "no_clients_found": "Клиентов не найдено", "general_statistics": "Общая статистика", - "number_of_dns_query_24_hours": "Количество DNS-запросов за 24 часа", "number_of_dns_query_blocked_24_hours": "Количество DNS-запросов, заблокированных фильтрами и блок-списками", "number_of_dns_query_blocked_24_hours_by_sec": "Количество DNS-запросов, заблокированных модулем Антифишинга AdGuard", "number_of_dns_query_blocked_24_hours_adult": "Количество заблокированных \"сайтов для взрослых\"", @@ -211,7 +208,7 @@ "install_devices_router_list_2": "Найдите настройки DHCP или DNS. Найдите буквы \"DNS\" рядом с текстовым полем, в которое можно ввести два или три ряда цифр, разделенных на 4 группы от одной до трёх цифр.", "install_devices_router_list_3": "Введите туда адрес вашего AdGuard Home.", "install_devices_windows_list_1": "Откройте Панель управления через меню \"Пуск\" или через поиск Windows.", - "install_devices_windows_list_2": "Откройте Панель управления через меню \"Пуск\" или через поиск Windows.", + "install_devices_windows_list_2": "Перейдите в \"Сеть и интернет\", а затем в \"Центр управления сетями и общим доступом\"", "install_devices_windows_list_3": "В левой стороне экрана найдите \"Изменение параметров адаптера\" и кликните по нему.", "install_devices_windows_list_4": "Выделите ваше активное подключение, затем кликните по нему правой клавишей мыши и выберите \"Свойства\".", "install_devices_windows_list_5": "Найдите в списке пункт \"IP версии 4 (TCP/IP)\", выделите его и затем снова нажмите \"Свойства\".", @@ -298,7 +295,6 @@ "client_deleted": "Клиент \"{{key}}\" успешно удален", "client_added": "Клиент \"{{key}}\" успешно добавлен", "client_updated": "Клиент \"{{key}}\" успешно обновлен", - "table_statistics": "Количество запросов (последние 24 часа)", "clients_not_found": "Клиентов не найдено", "client_confirm_delete": "Вы уверены, что хотите удалить клиента \"{{key}}\"?", "filter_confirm_delete": "Вы уверены, что хотите удалить фильтр?", @@ -309,7 +305,7 @@ "access_allowed_title": "Разрешенные клиенты", "access_allowed_desc": "Список CIDR- или IP-адресов. Если он настроен, AdGuard Home будет принимать запросы только с этих IP-адресов.", "access_disallowed_title": "Запрещенные клиенты", - "access_disallowed_desc": "Список CIDR- или IP-адресов. Если он настроек, AdGuard Home будет игнорировать запросы с этих IP-адресов.", + "access_disallowed_desc": "Список CIDR- или IP-адресов. Если он настроен, AdGuard Home будет игнорировать запросы с этих IP-адресов.", "access_blocked_title": "Заблокированные домены", "access_blocked_desc": "Не путайте это с фильтрами. AdGuard Home будет игнорировать DNS-запросы с этими доменами.", "access_settings_saved": "Настройки доступа успешно сохранены", @@ -353,5 +349,13 @@ "blocked_services_global": "Использовать глобальные заблокированные сервисы", "blocked_service": "Заблокированный сервис", "block_all": "Заблокировать все", - "unblock_all": "Разблокировать все" + "unblock_all": "Разблокировать все", + "domain": "Домен", + "answer": "Ответ", + "interval_hours_0": "{{count}} час", + "interval_hours_1": "{{count}} часа", + "interval_hours_2": "{{count}} часов", + "interval_days_0": "{{count}} день", + "interval_days_1": "{{count}} дня", + "interval_days_2": "{{count}} дней" } \ No newline at end of file diff --git a/client/src/actions/filtering.js b/client/src/actions/filtering.js new file mode 100644 index 00000000..3f0c59d9 --- /dev/null +++ b/client/src/actions/filtering.js @@ -0,0 +1,145 @@ +import { createAction } from 'redux-actions'; +import { showLoading, hideLoading } from 'react-redux-loading-bar'; + +import { normalizeFilteringStatus, normalizeRulesTextarea } from '../helpers/helpers'; +import { addErrorToast, addSuccessToast } from './index'; +import apiClient from '../api/Api'; + +export const toggleFilteringModal = createAction('FILTERING_MODAL_TOGGLE'); +export const handleRulesChange = createAction('HANDLE_RULES_CHANGE'); + +export const getFilteringStatusRequest = createAction('GET_FILTERING_STATUS_REQUEST'); +export const getFilteringStatusFailure = createAction('GET_FILTERING_STATUS_FAILURE'); +export const getFilteringStatusSuccess = createAction('GET_FILTERING_STATUS_SUCCESS'); + +export const getFilteringStatus = () => async (dispatch) => { + dispatch(getFilteringStatusRequest()); + try { + const status = await apiClient.getFilteringStatus(); + dispatch(getFilteringStatusSuccess({ ...normalizeFilteringStatus(status) })); + } catch (error) { + dispatch(addErrorToast({ error })); + dispatch(getFilteringStatusFailure()); + } +}; + +export const setRulesRequest = createAction('SET_RULES_REQUEST'); +export const setRulesFailure = createAction('SET_RULES_FAILURE'); +export const setRulesSuccess = createAction('SET_RULES_SUCCESS'); + +export const setRules = rules => async (dispatch) => { + dispatch(setRulesRequest()); + try { + const normalizedRules = normalizeRulesTextarea(rules); + await apiClient.setRules(normalizedRules); + dispatch(addSuccessToast('updated_custom_filtering_toast')); + dispatch(setRulesSuccess()); + } catch (error) { + dispatch(addErrorToast({ error })); + dispatch(setRulesFailure()); + } +}; + +export const addFilterRequest = createAction('ADD_FILTER_REQUEST'); +export const addFilterFailure = createAction('ADD_FILTER_FAILURE'); +export const addFilterSuccess = createAction('ADD_FILTER_SUCCESS'); + +export const addFilter = (url, name) => async (dispatch) => { + dispatch(addFilterRequest()); + try { + await apiClient.addFilter(url, name); + dispatch(addFilterSuccess(url)); + dispatch(toggleFilteringModal()); + dispatch(addSuccessToast('filter_added_successfully')); + dispatch(getFilteringStatus()); + } catch (error) { + dispatch(addErrorToast({ error })); + dispatch(addFilterFailure()); + } +}; + +export const removeFilterRequest = createAction('REMOVE_FILTER_REQUEST'); +export const removeFilterFailure = createAction('REMOVE_FILTER_FAILURE'); +export const removeFilterSuccess = createAction('REMOVE_FILTER_SUCCESS'); + +export const removeFilter = url => async (dispatch) => { + dispatch(removeFilterRequest()); + try { + await apiClient.removeFilter(url); + dispatch(removeFilterSuccess(url)); + dispatch(getFilteringStatus()); + } catch (error) { + dispatch(addErrorToast({ error })); + dispatch(removeFilterFailure()); + } +}; + +export const toggleFilterRequest = createAction('FILTER_TOGGLE_REQUEST'); +export const toggleFilterFailure = createAction('FILTER_TOGGLE_FAILURE'); +export const toggleFilterSuccess = createAction('FILTER_TOGGLE_SUCCESS'); + +export const toggleFilterStatus = (url, enabled) => async (dispatch) => { + dispatch(toggleFilterRequest()); + try { + await apiClient.setFilterUrl({ url, enabled: !enabled }); + dispatch(toggleFilterSuccess(url)); + dispatch(getFilteringStatus()); + } catch (error) { + dispatch(addErrorToast({ error })); + dispatch(toggleFilterFailure()); + } +}; + +export const refreshFiltersRequest = createAction('FILTERING_REFRESH_REQUEST'); +export const refreshFiltersFailure = createAction('FILTERING_REFRESH_FAILURE'); +export const refreshFiltersSuccess = createAction('FILTERING_REFRESH_SUCCESS'); + +export const refreshFilters = () => async (dispatch) => { + dispatch(refreshFiltersRequest()); + dispatch(showLoading()); + try { + const refreshText = await apiClient.refreshFilters(); + dispatch(refreshFiltersSuccess()); + + if (refreshText.includes('OK')) { + if (refreshText.includes('OK 0')) { + dispatch(addSuccessToast('all_filters_up_to_date_toast')); + } else { + dispatch(addSuccessToast(refreshText.replace(/OK /g, ''))); + } + } else { + dispatch(addErrorToast({ error: refreshText })); + } + + dispatch(getFilteringStatus()); + dispatch(hideLoading()); + } catch (error) { + dispatch(addErrorToast({ error })); + dispatch(refreshFiltersFailure()); + dispatch(hideLoading()); + } +}; + +export const setFiltersConfigRequest = createAction('SET_FILTERS_CONFIG_REQUEST'); +export const setFiltersConfigFailure = createAction('SET_FILTERS_CONFIG_FAILURE'); +export const setFiltersConfigSuccess = createAction('SET_FILTERS_CONFIG_SUCCESS'); + +export const setFiltersConfig = config => async (dispatch, getState) => { + dispatch(setFiltersConfigRequest()); + try { + const { enabled } = config; + const prevEnabled = getState().filtering.enabled; + let successToastMessage = 'config_successfully_saved'; + + if (prevEnabled !== enabled) { + successToastMessage = enabled ? 'enabled_filtering_toast' : 'disabled_filtering_toast'; + } + + await apiClient.setFiltersConfig(config); + dispatch(addSuccessToast(successToastMessage)); + dispatch(setFiltersConfigSuccess(config)); + } catch (error) { + dispatch(addErrorToast({ error })); + dispatch(setFiltersConfigFailure()); + } +}; diff --git a/client/src/actions/index.js b/client/src/actions/index.js index 8a132060..88e14ef9 100644 --- a/client/src/actions/index.js +++ b/client/src/actions/index.js @@ -1,10 +1,9 @@ import { createAction } from 'redux-actions'; import { t } from 'i18next'; -import { showLoading, hideLoading } from 'react-redux-loading-bar'; import axios from 'axios'; import versionCompare from '../helpers/versionCompare'; -import { normalizeFilteringStatus, normalizeTextarea, sortClients } from '../helpers/helpers'; +import { normalizeTextarea, sortClients } from '../helpers/helpers'; import { SETTINGS_NAMES, CHECK_TIMEOUT } from '../helpers/constants'; import { getTlsStatus } from './encryption'; import apiClient from '../api/Api'; @@ -21,16 +20,6 @@ export const toggleSetting = (settingKey, status) => async (dispatch) => { let successMessage = ''; try { switch (settingKey) { - case SETTINGS_NAMES.filtering: - if (status) { - successMessage = 'disabled_filtering_toast'; - await apiClient.disableFiltering(); - } else { - successMessage = 'enabled_filtering_toast'; - await apiClient.enableFiltering(); - } - dispatch(toggleSettingStatus({ settingKey })); - break; case SETTINGS_NAMES.safebrowsing: if (status) { successMessage = 'disabled_safe_browsing_toast'; @@ -77,18 +66,15 @@ export const initSettingsSuccess = createAction('SETTINGS_INIT_SUCCESS'); export const initSettings = settingsList => async (dispatch) => { dispatch(initSettingsRequest()); try { - const filteringStatus = await apiClient.getFilteringStatus(); const safebrowsingStatus = await apiClient.getSafebrowsingStatus(); const parentalStatus = await apiClient.getParentalStatus(); const safesearchStatus = await apiClient.getSafesearchStatus(); const { - filtering, safebrowsing, parental, safesearch, } = settingsList; const newSettingsList = { - filtering: { ...filtering, enabled: filteringStatus.enabled }, safebrowsing: { ...safebrowsing, enabled: safebrowsingStatus.enabled }, parental: { ...parental, enabled: parentalStatus.enabled }, safesearch: { ...safesearch, enabled: safesearchStatus.enabled }, @@ -100,21 +86,6 @@ export const initSettings = settingsList => async (dispatch) => { } }; -export const getFilteringRequest = createAction('GET_FILTERING_REQUEST'); -export const getFilteringFailure = createAction('GET_FILTERING_FAILURE'); -export const getFilteringSuccess = createAction('GET_FILTERING_SUCCESS'); - -export const getFiltering = () => async (dispatch) => { - dispatch(getFilteringRequest()); - try { - const filteringStatus = await apiClient.getFilteringStatus(); - dispatch(getFilteringSuccess(filteringStatus.enabled)); - } catch (error) { - dispatch(addErrorToast({ error })); - dispatch(getFilteringFailure()); - } -}; - export const toggleProtectionRequest = createAction('TOGGLE_PROTECTION_REQUEST'); export const toggleProtectionFailure = createAction('TOGGLE_PROTECTION_FAILURE'); export const toggleProtectionSuccess = createAction('TOGGLE_PROTECTION_SUCCESS'); @@ -290,133 +261,6 @@ export const disableDns = () => async (dispatch) => { } }; -export const setRulesRequest = createAction('SET_RULES_REQUEST'); -export const setRulesFailure = createAction('SET_RULES_FAILURE'); -export const setRulesSuccess = createAction('SET_RULES_SUCCESS'); - -export const setRules = rules => async (dispatch) => { - dispatch(setRulesRequest()); - try { - const replacedLineEndings = rules - .replace(/^\n/g, '') - .replace(/\n\s*\n/g, '\n'); - await apiClient.setRules(replacedLineEndings); - dispatch(addSuccessToast('updated_custom_filtering_toast')); - dispatch(setRulesSuccess()); - } catch (error) { - dispatch(addErrorToast({ error })); - dispatch(setRulesFailure()); - } -}; - -export const getFilteringStatusRequest = createAction('GET_FILTERING_STATUS_REQUEST'); -export const getFilteringStatusFailure = createAction('GET_FILTERING_STATUS_FAILURE'); -export const getFilteringStatusSuccess = createAction('GET_FILTERING_STATUS_SUCCESS'); - -export const getFilteringStatus = () => async (dispatch) => { - dispatch(getFilteringStatusRequest()); - try { - const status = await apiClient.getFilteringStatus(); - dispatch(getFilteringStatusSuccess({ status: normalizeFilteringStatus(status) })); - } catch (error) { - dispatch(addErrorToast({ error })); - dispatch(getFilteringStatusFailure()); - } -}; - -export const toggleFilterRequest = createAction('FILTER_ENABLE_REQUEST'); -export const toggleFilterFailure = createAction('FILTER_ENABLE_FAILURE'); -export const toggleFilterSuccess = createAction('FILTER_ENABLE_SUCCESS'); - -export const toggleFilterStatus = url => async (dispatch, getState) => { - dispatch(toggleFilterRequest()); - const state = getState(); - const { filters } = state.filtering; - const filter = filters.filter(filter => filter.url === url)[0]; - const { enabled } = filter; - let toggleStatusMethod; - if (enabled) { - toggleStatusMethod = apiClient.disableFilter.bind(apiClient); - } else { - toggleStatusMethod = apiClient.enableFilter.bind(apiClient); - } - try { - await toggleStatusMethod(url); - dispatch(toggleFilterSuccess(url)); - dispatch(getFilteringStatus()); - } catch (error) { - dispatch(addErrorToast({ error })); - dispatch(toggleFilterFailure()); - } -}; - -export const refreshFiltersRequest = createAction('FILTERING_REFRESH_REQUEST'); -export const refreshFiltersFailure = createAction('FILTERING_REFRESH_FAILURE'); -export const refreshFiltersSuccess = createAction('FILTERING_REFRESH_SUCCESS'); - -export const refreshFilters = () => async (dispatch) => { - dispatch(refreshFiltersRequest()); - dispatch(showLoading()); - try { - const refreshText = await apiClient.refreshFilters(); - dispatch(refreshFiltersSuccess()); - - if (refreshText.includes('OK')) { - if (refreshText.includes('OK 0')) { - dispatch(addSuccessToast('all_filters_up_to_date_toast')); - } else { - dispatch(addSuccessToast(refreshText.replace(/OK /g, ''))); - } - } else { - dispatch(addErrorToast({ error: refreshText })); - } - - dispatch(getFilteringStatus()); - dispatch(hideLoading()); - } catch (error) { - dispatch(addErrorToast({ error })); - dispatch(refreshFiltersFailure()); - dispatch(hideLoading()); - } -}; - -export const handleRulesChange = createAction('HANDLE_RULES_CHANGE'); - -export const addFilterRequest = createAction('ADD_FILTER_REQUEST'); -export const addFilterFailure = createAction('ADD_FILTER_FAILURE'); -export const addFilterSuccess = createAction('ADD_FILTER_SUCCESS'); - -export const addFilter = (url, name) => async (dispatch) => { - dispatch(addFilterRequest()); - try { - await apiClient.addFilter(url, name); - dispatch(addFilterSuccess(url)); - dispatch(getFilteringStatus()); - } catch (error) { - dispatch(addErrorToast({ error })); - dispatch(addFilterFailure()); - } -}; - - -export const removeFilterRequest = createAction('ADD_FILTER_REQUEST'); -export const removeFilterFailure = createAction('ADD_FILTER_FAILURE'); -export const removeFilterSuccess = createAction('ADD_FILTER_SUCCESS'); - -export const removeFilter = url => async (dispatch) => { - dispatch(removeFilterRequest()); - try { - await apiClient.removeFilter(url); - dispatch(removeFilterSuccess(url)); - dispatch(getFilteringStatus()); - } catch (error) { - dispatch(addErrorToast({ error })); - dispatch(removeFilterFailure()); - } -}; - -export const toggleFilteringModal = createAction('FILTERING_MODAL_TOGGLE'); - export const handleUpstreamChange = createAction('HANDLE_UPSTREAM_CHANGE'); export const setUpstreamRequest = createAction('SET_UPSTREAM_REQUEST'); export const setUpstreamFailure = createAction('SET_UPSTREAM_FAILURE'); diff --git a/client/src/api/Api.js b/client/src/api/Api.js index f39b28dc..187b7312 100644 --- a/client/src/api/Api.js +++ b/client/src/api/Api.js @@ -90,32 +90,19 @@ class Api { } // Filtering - FILTERING_STATUS = { path: 'filtering/status', method: 'GET' }; - FILTERING_ENABLE = { path: 'filtering/enable', method: 'POST' }; - FILTERING_DISABLE = { path: 'filtering/disable', method: 'POST' }; + FILTERING_INFO = { path: 'filtering_info', method: 'GET' }; FILTERING_ADD_FILTER = { path: 'filtering/add_url', method: 'POST' }; FILTERING_REMOVE_FILTER = { path: 'filtering/remove_url', method: 'POST' }; FILTERING_SET_RULES = { path: 'filtering/set_rules', method: 'POST' }; - FILTERING_ENABLE_FILTER = { path: 'filtering/enable_url', method: 'POST' }; - FILTERING_DISABLE_FILTER = { path: 'filtering/disable_url', method: 'POST' }; FILTERING_REFRESH = { path: 'filtering/refresh', method: 'POST' }; + FILTERING_SET_URL = { path: 'filtering/set_url', method: 'POST' }; + FILTERING_CONFIG = { path: 'filtering_config', method: 'POST' }; getFilteringStatus() { - const { path, method } = this.FILTERING_STATUS; + const { path, method } = this.FILTERING_INFO; return this.makeRequest(path, method); } - enableFiltering() { - const { path, method } = this.FILTERING_ENABLE; - return this.makeRequest(path, method); - } - - disableFiltering() { - const { path, method } = this.FILTERING_DISABLE; - return this.makeRequest(path, method); - } - - // TODO find out when to use force parameter refreshFilters() { const { path, method } = this.FILTERING_REFRESH; return this.makeRequest(path, method); @@ -151,26 +138,22 @@ class Api { return this.makeRequest(path, method, parameters); } - enableFilter(url) { - const { path, method } = this.FILTERING_ENABLE_FILTER; - const parameter = 'url'; - const requestBody = `${parameter}=${url}`; - const config = { - data: requestBody, - header: { 'Content-Type': 'text/plain' }, + setFiltersConfig(config) { + const { path, method } = this.FILTERING_CONFIG; + const parameters = { + data: config, + headers: { 'Content-Type': 'application/json' }, }; - return this.makeRequest(path, method, config); + return this.makeRequest(path, method, parameters); } - disableFilter(url) { - const { path, method } = this.FILTERING_DISABLE_FILTER; - const parameter = 'url'; - const requestBody = `${parameter}=${url}`; - const config = { - data: requestBody, - header: { 'Content-Type': 'text/plain' }, + setFilterUrl(config) { + const { path, method } = this.FILTERING_SET_URL; + const parameters = { + data: config, + headers: { 'Content-Type': 'application/json' }, }; - return this.makeRequest(path, method, config); + return this.makeRequest(path, method, parameters); } // Parental diff --git a/client/src/components/Filters/Modal.js b/client/src/components/Filters/Modal.js index 884117b7..c4b89437 100644 --- a/client/src/components/Filters/Modal.js +++ b/client/src/components/Filters/Modal.js @@ -33,27 +33,13 @@ class Modal extends Component { this.setState({ ...this.state, name }); }; - handleNext = () => { - this.props.addFilter(this.state.url, this.state.name); - setTimeout(() => { - if (this.props.isFilterAdded) { - this.closeModal(); - } - }, 2000); - }; - closeModal = () => { this.props.toggleModal(); this.setState({ ...this.state, ...initialState }); - } + }; render() { - const { - isOpen, - title, - inputDescription, - processingAddFilter, - } = this.props; + const { isOpen, processingAddFilter } = this.props; const { isUrlValid, url, name } = this.state; const inputUrlClass = classnames({ 'form-control mb-2': true, @@ -64,28 +50,7 @@ class Modal extends Component { 'form-control mb-2': true, 'is-valid': name.length > 0, }); - - const renderBody = () => { - if (!this.props.isFilterAdded) { - return ( - <React.Fragment> - <input type="text" className={inputNameClass} placeholder={this.props.t('enter_name_hint')} onChange={this.handleNameChange} /> - <input type="text" className={inputUrlClass} placeholder={this.props.t('enter_url_hint')} onChange={this.handleUrlChange} /> - {inputDescription && - <div className="description"> - {inputDescription} - </div>} - </React.Fragment> - ); - } - return ( - <div className="description"> - <Trans>filter_added_successfully</Trans> - </div> - ); - }; - - const isValidForSubmit = !(url.length > 0 && isUrlValid && name.length > 0); + const isValidForSubmit = url.length > 0 && isUrlValid && name.length > 0; return ( <ReactModal @@ -96,35 +61,47 @@ class Modal extends Component { > <div className="modal-content"> <div className="modal-header"> - <h4 className="modal-title"> - {title} - </h4> - <button type="button" className="close" onClick={this.closeModal}> - <span className="sr-only">Close</span> - </button> + <h4 className="modal-title"> + <Trans>new_filter_btn</Trans> + </h4> + <button type="button" className="close" onClick={this.closeModal}> + <span className="sr-only">Close</span> + </button> </div> <div className="modal-body"> - {renderBody()} - </div> - {!this.props.isFilterAdded && - <div className="modal-footer"> - <button - type="button" - className="btn btn-secondary" - onClick={this.closeModal} - > - <Trans>cancel_btn</Trans> - </button> - <button - type="button" - className="btn btn-success" - onClick={this.handleNext} - disabled={isValidForSubmit || processingAddFilter} - > - <Trans>add_filter_btn</Trans> - </button> + <input + type="text" + className={inputNameClass} + placeholder={this.props.t('enter_name_hint')} + onChange={this.handleNameChange} + /> + <input + type="text" + className={inputUrlClass} + placeholder={this.props.t('enter_url_hint')} + onChange={this.handleUrlChange} + /> + <div className="description"> + <Trans>enter_valid_filter_url</Trans> </div> - } + </div> + <div className="modal-footer"> + <button + type="button" + className="btn btn-secondary" + onClick={this.closeModal} + > + <Trans>cancel_btn</Trans> + </button> + <button + type="button" + className="btn btn-success" + onClick={() => this.props.addFilter(url, name)} + disabled={!isValidForSubmit || processingAddFilter} + > + <Trans>add_filter_btn</Trans> + </button> + </div> </div> </ReactModal> ); @@ -134,12 +111,10 @@ class Modal extends Component { Modal.propTypes = { toggleModal: PropTypes.func.isRequired, isOpen: PropTypes.bool.isRequired, - title: PropTypes.string.isRequired, - inputDescription: PropTypes.string, addFilter: PropTypes.func.isRequired, - isFilterAdded: PropTypes.bool, - processingAddFilter: PropTypes.bool, - t: PropTypes.func, + isFilterAdded: PropTypes.bool.isRequired, + processingAddFilter: PropTypes.bool.isRequired, + t: PropTypes.func.isRequired, }; export default withNamespaces()(Modal); diff --git a/client/src/components/Filters/UserRules.js b/client/src/components/Filters/UserRules.js index 39d39351..d2d78e91 100644 --- a/client/src/components/Filters/UserRules.js +++ b/client/src/components/Filters/UserRules.js @@ -15,13 +15,13 @@ class UserRules extends Component { }; render() { - const { t } = this.props; + const { t, userRules } = this.props; return ( <Card title={t('custom_filter_rules')} subtitle={t('custom_filter_rules_hint')}> <form onSubmit={this.handleSubmit}> <textarea className="form-control form-control--textarea-large" - value={this.props.userRules} + value={userRules} onChange={this.handleChange} /> <div className="card-actions"> @@ -79,10 +79,10 @@ class UserRules extends Component { } UserRules.propTypes = { - userRules: PropTypes.string, - handleRulesChange: PropTypes.func, - handleRulesSubmit: PropTypes.func, - t: PropTypes.func, + userRules: PropTypes.string.isRequired, + handleRulesChange: PropTypes.func.isRequired, + handleRulesSubmit: PropTypes.func.isRequired, + t: PropTypes.func.isRequired, }; export default withNamespaces()(UserRules); diff --git a/client/src/components/Filters/index.js b/client/src/components/Filters/index.js index b95745ab..ba8213e5 100644 --- a/client/src/components/Filters/index.js +++ b/client/src/components/Filters/index.js @@ -1,11 +1,13 @@ -import React, { Component } from 'react'; +import React, { Component, Fragment } from 'react'; import ReactTable from 'react-table'; import PropTypes from 'prop-types'; import { Trans, withNamespaces } from 'react-i18next'; -import Modal from './Modal'; + import PageTitle from '../ui/PageTitle'; import Card from '../ui/Card'; +import CellWrap from '../ui/CellWrap'; import UserRules from './UserRules'; +import Modal from './Modal'; class Filters extends Component { componentDidMount() { @@ -20,14 +22,19 @@ class Filters extends Component { this.props.setRules(this.props.filtering.userRules); }; - renderCheckbox = (row) => { - const { url } = row.original; - const { filters } = this.props.filtering; - const filter = filters.filter(filter => filter.url === url)[0]; + renderCheckbox = ({ original }) => { + const { processingConfigFilter } = this.props.filtering; + const { url, enabled } = original; return ( <label className="checkbox"> - <input type="checkbox" className="checkbox__input" onChange={() => this.props.toggleFilterStatus(filter.url)} checked={filter.enabled}/> - <span className="checkbox__label"/> + <input + type="checkbox" + className="checkbox__input" + onChange={() => this.props.toggleFilterStatus(url, enabled)} + checked={enabled} + disabled={processingConfigFilter} + /> + <span className="checkbox__label" /> </label> ); }; @@ -37,92 +44,131 @@ class Filters extends Component { if (window.confirm(this.props.t('filter_confirm_delete'))) { this.props.removeFilter({ url }); } - } + }; - columns = [{ - Header: <Trans>enabled_table_header</Trans>, - accessor: 'enabled', - Cell: this.renderCheckbox, - width: 90, - className: 'text-center', - }, { - Header: <Trans>name_table_header</Trans>, - accessor: 'name', - Cell: ({ value }) => (<div className="logs__row logs__row--overflow"><span className="logs__text" title={value}>{value}</span></div>), - }, { - Header: <Trans>filter_url_table_header</Trans>, - accessor: 'url', - Cell: ({ value }) => (<div className="logs__row logs__row--overflow"><a href={value} target='_blank' rel='noopener noreferrer' className="link logs__text">{value}</a></div>), - }, { - Header: <Trans>rules_count_table_header</Trans>, - accessor: 'rulesCount', - className: 'text-center', - Cell: props => props.value.toLocaleString(), - }, { - Header: <Trans>last_time_updated_table_header</Trans>, - accessor: 'lastUpdated', - className: 'text-center', - }, { - Header: <Trans>actions_table_header</Trans>, - accessor: 'url', - Cell: ({ value }) => ( - <button - type="button" - className="btn btn-icon btn-outline-secondary btn-sm" - onClick={() => this.handleDelete(value)} - title={this.props.t('delete_table_action')} - > - <svg className="icons"> - <use xlinkHref="#delete" /> - </svg> - </button> - ), - className: 'text-center', - width: 80, - sortable: false, - }, + columns = [ + { + Header: <Trans>enabled_table_header</Trans>, + accessor: 'enabled', + Cell: this.renderCheckbox, + width: 90, + className: 'text-center', + }, + { + Header: <Trans>name_table_header</Trans>, + accessor: 'name', + minWidth: 200, + Cell: CellWrap, + }, + { + Header: <Trans>filter_url_table_header</Trans>, + accessor: 'url', + minWidth: 200, + Cell: ({ value }) => ( + <div className="logs__row logs__row--overflow"> + <a + href={value} + target="_blank" + rel="noopener noreferrer" + className="link logs__text" + > + {value} + </a> + </div> + ), + }, + { + Header: <Trans>rules_count_table_header</Trans>, + accessor: 'rulesCount', + className: 'text-center', + minWidth: 100, + Cell: props => props.value.toLocaleString(), + }, + { + Header: <Trans>last_time_updated_table_header</Trans>, + accessor: 'lastUpdated', + className: 'text-center', + minWidth: 150, + Cell: CellWrap, + }, + { + Header: <Trans>actions_table_header</Trans>, + accessor: 'url', + Cell: ({ value }) => ( + <button + type="button" + className="btn btn-icon btn-outline-secondary btn-sm" + onClick={() => this.handleDelete(value)} + title={this.props.t('delete_table_action')} + > + <svg className="icons"> + <use xlinkHref="#delete" /> + </svg> + </button> + ), + className: 'text-center', + width: 80, + sortable: false, + }, ]; render() { - const { t } = this.props; - const { filters, userRules, processingRefreshFilters } = this.props.filtering; + const { + filtering, t, toggleFilteringModal, refreshFilters, addFilter, + } = this.props; + const { + filters, + userRules, + isModalOpen, + isFilterAdded, + processingRefreshFilters, + processingRemoveFilter, + processingAddFilter, + processingFilters, + } = filtering; + return ( - <div> - <PageTitle title={ t('filters') } /> + <Fragment> + <PageTitle title={t('filters')} /> <div className="content"> <div className="row"> <div className="col-md-12"> <Card - title={ t('filters_and_hosts') } - subtitle={ t('filters_and_hosts_hint') } + title={t('filters_and_hosts')} + subtitle={t('filters_and_hosts_hint')} > <ReactTable data={filters} columns={this.columns} showPagination={true} defaultPageSize={10} + loading={ + processingFilters || + processingAddFilter || + processingRemoveFilter || + processingRefreshFilters + } minRows={4} - // Text - previousText={ t('previous_btn') } - nextText={ t('next_btn') } - loadingText={ t('loading_table_status') } - pageText={ t('page_table_footer_text') } - ofText={ t('of_table_footer_text') } - rowsText={ t('rows_table_footer_text') } - noDataText={ t('no_filters_added') } + previousText={t('previous_btn')} + nextText={t('next_btn')} + loadingText={t('loading_table_status')} + pageText={t('page_table_footer_text')} + ofText={t('of_table_footer_text')} + rowsText={t('rows_table_footer_text')} + noDataText={t('no_filters_added')} /> <div className="card-actions"> <button className="btn btn-success btn-standard mr-2" type="submit" - onClick={this.props.toggleFilteringModal} + onClick={toggleFilteringModal} > <Trans>add_filter_btn</Trans> </button> <button className="btn btn-primary btn-standard" type="submit" - onClick={this.props.refreshFilters} + onClick={refreshFilters} disabled={processingRefreshFilters} > <Trans>check_updates_btn</Trans> @@ -140,15 +186,13 @@ class Filters extends Component { </div> </div> <Modal - isOpen={this.props.filtering.isFilteringModalOpen} - toggleModal={this.props.toggleFilteringModal} - addFilter={this.props.addFilter} - isFilterAdded={this.props.filtering.isFilterAdded} - processingAddFilter={this.props.filtering.processingAddFilter} - title={ t('new_filter_btn') } - inputDescription={ t('enter_valid_filter_url') } + isOpen={isModalOpen} + toggleModal={toggleFilteringModal} + addFilter={addFilter} + isFilterAdded={isFilterAdded} + processingAddFilter={processingAddFilter} /> - </div> + </Fragment> ); } } @@ -157,12 +201,15 @@ Filters.propTypes = { setRules: PropTypes.func, getFilteringStatus: PropTypes.func.isRequired, filtering: PropTypes.shape({ - userRules: PropTypes.string, - filters: PropTypes.array, - isFilteringModalOpen: PropTypes.bool.isRequired, - isFilterAdded: PropTypes.bool, - processingAddFilter: PropTypes.bool, - processingRefreshFilters: PropTypes.bool, + userRules: PropTypes.string.isRequired, + filters: PropTypes.array.isRequired, + isModalOpen: PropTypes.bool.isRequired, + isFilterAdded: PropTypes.bool.isRequired, + processingFilters: PropTypes.bool.isRequired, + processingAddFilter: PropTypes.bool.isRequired, + processingRefreshFilters: PropTypes.bool.isRequired, + processingConfigFilter: PropTypes.bool.isRequired, + processingRemoveFilter: PropTypes.bool.isRequired, }), removeFilter: PropTypes.func.isRequired, toggleFilterStatus: PropTypes.func.isRequired, @@ -170,8 +217,7 @@ Filters.propTypes = { toggleFilteringModal: PropTypes.func.isRequired, handleRulesChange: PropTypes.func.isRequired, refreshFilters: PropTypes.func.isRequired, - t: PropTypes.func, + t: PropTypes.func.isRequired, }; - export default withNamespaces()(Filters); diff --git a/client/src/components/Logs/Logs.css b/client/src/components/Logs/Logs.css index 313879b6..a5c073af 100644 --- a/client/src/components/Logs/Logs.css +++ b/client/src/components/Logs/Logs.css @@ -36,6 +36,10 @@ overflow: hidden; } +.logs__text--full { + width: 100%; +} + .logs__row .tooltip-custom { top: 0; margin-left: 0; diff --git a/client/src/components/Logs/index.js b/client/src/components/Logs/index.js index e4f880b0..3f1493af 100644 --- a/client/src/components/Logs/index.js +++ b/client/src/components/Logs/index.js @@ -6,7 +6,7 @@ import endsWith from 'lodash/endsWith'; import { Trans, withNamespaces } from 'react-i18next'; import { HashLink as Link } from 'react-router-hash-link'; -import { formatTime, getClientName } from '../../helpers/helpers'; +import { formatTime, formatDateTime, getClientName } from '../../helpers/helpers'; import { SERVICES, FILTERED_STATUS } from '../../helpers/constants'; import { getTrackerData } from '../../helpers/trackers/trackers'; import PageTitle from '../ui/PageTitle'; @@ -114,7 +114,7 @@ class Logs extends Component { getTimeCell = ({ value }) => ( <div className="logs__row"> - <span className="logs__text" title={value}> + <span className="logs__text" title={formatDateTime(value)}> {formatTime(value)} </span> </div> @@ -227,7 +227,7 @@ class Logs extends Component { { Header: t('time_table_header'), accessor: 'time', - maxWidth: 90, + maxWidth: 100, filterable: false, Cell: this.getTimeCell, }, diff --git a/client/src/components/Settings/FiltersConfig/Form.js b/client/src/components/Settings/FiltersConfig/Form.js new file mode 100644 index 00000000..1a5add1e --- /dev/null +++ b/client/src/components/Settings/FiltersConfig/Form.js @@ -0,0 +1,88 @@ +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 { renderSelectField, toNumber } from '../../../helpers/form'; +import { FILTERS_INTERVALS_HOURS } from '../../../helpers/constants'; + +const getTitleForInterval = (interval, t) => { + if (interval === 0) { + return t('disabled'); + } else if (interval === 72 || interval === 168) { + return t('interval_days', { count: interval / 24 }); + } + + return t('interval_hours', { count: interval }); +}; + +const getIntervalSelect = (processing, t, handleChange, toNumber) => ( + <Field + name="interval" + className="custom-select" + component="select" + onChange={handleChange} + normalize={toNumber} + disabled={processing} + > + {FILTERS_INTERVALS_HOURS.map(interval => ( + <option value={interval} key={interval}> + {getTitleForInterval(interval, t)} + </option> + ))} + </Field> +); + +const Form = (props) => { + const { + handleSubmit, handleChange, processing, t, + } = props; + + return ( + <form onSubmit={handleSubmit}> + <div className="row"> + <div className="col-12"> + <div className="form__group form__group--settings"> + <Field + name="enabled" + type="checkbox" + modifier="checkbox--settings" + component={renderSelectField} + placeholder={t('block_domain_use_filters_and_hosts')} + subtitle={t('filters_block_toggle_hint')} + onChange={handleChange} + disabled={processing} + /> + </div> + </div> + <div className="col-12 col-md-5"> + <div className="form__group form__group--inner mb-5"> + <label className="form__label"> + <Trans>filters_interval</Trans> + </label> + + {getIntervalSelect(processing, t, handleChange, toNumber)} + </div> + </div> + </div> + </form> + ); +}; + +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: 'filterConfigForm', + }), +])(Form); diff --git a/client/src/components/Settings/FiltersConfig/index.js b/client/src/components/Settings/FiltersConfig/index.js new file mode 100644 index 00000000..c1ae5798 --- /dev/null +++ b/client/src/components/Settings/FiltersConfig/index.js @@ -0,0 +1,36 @@ +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'; + +class FiltersConfig extends Component { + handleFormChange = debounce((values) => { + this.props.setFiltersConfig(values); + }, DEBOUNCE_TIMEOUT); + + render() { + const { interval, enabled, processing } = this.props; + + return ( + <Form + initialValues={{ interval, enabled }} + onSubmit={this.handleFormChange} + onChange={this.handleFormChange} + processing={processing} + /> + ); + } +} + +FiltersConfig.propTypes = { + interval: PropTypes.number.isRequired, + enabled: PropTypes.bool.isRequired, + processing: PropTypes.bool.isRequired, + setFiltersConfig: PropTypes.func.isRequired, + t: PropTypes.func.isRequired, +}; + +export default withNamespaces()(FiltersConfig); diff --git a/client/src/components/Settings/Settings.css b/client/src/components/Settings/Settings.css index 0cc18f6e..284d70e2 100644 --- a/client/src/components/Settings/Settings.css +++ b/client/src/components/Settings/Settings.css @@ -11,6 +11,17 @@ margin-bottom: 20px; } +.form__group--inner { + max-width: 300px; + margin-top: -10px; + margin-left: 40px; + font-size: 14px; +} + +.form__group--checkbox { + margin-bottom: 25px; +} + .form__inline { display: flex; justify-content: flex-start; @@ -109,3 +120,11 @@ .custom-control-label:before { transition: 0.3s ease-in-out background-color, 0.3s ease-in-out color; } + +.custom-select:disabled { + background-color: #f9f9f9; +} + +.custom-select { + transition: 0.3s ease-in-out background-color, 0.3s ease-in-out color; +} diff --git a/client/src/components/Settings/index.js b/client/src/components/Settings/index.js index 2ca90782..936e05c9 100644 --- a/client/src/components/Settings/index.js +++ b/client/src/components/Settings/index.js @@ -5,6 +5,7 @@ import { withNamespaces } from 'react-i18next'; import Services from './Services'; import StatsConfig from './StatsConfig'; import LogsConfig from './LogsConfig'; +import FiltersConfig from './FiltersConfig'; import Checkbox from '../ui/Checkbox'; import Loading from '../ui/Loading'; import PageTitle from '../ui/PageTitle'; @@ -14,11 +15,6 @@ import './Settings.css'; class Settings extends Component { settings = { - filtering: { - enabled: false, - title: 'block_domain_use_filters_and_hosts', - subtitle: 'filters_block_toggle_hint', - }, safebrowsing: { enabled: false, title: 'use_adguard_browsing_sec', @@ -41,17 +37,20 @@ class Settings extends Component { this.props.getBlockedServices(); this.props.getStatsConfig(); this.props.getLogsConfig(); + this.props.getFilteringStatus(); } renderSettings = (settings) => { - if (Object.keys(settings).length > 0) { - return Object.keys(settings).map((key) => { + const settingsKeys = Object.keys(settings); + + if (settingsKeys.length > 0) { + return settingsKeys.map((key) => { const setting = settings[key]; const { enabled } = setting; return ( <Checkbox - key={key} {...settings[key]} + key={key} handleChange={() => this.props.toggleSetting(key, enabled)} /> ); @@ -71,6 +70,8 @@ class Settings extends Component { queryLogs, setLogsConfig, clearLogs, + filtering, + setFiltersConfig, t, } = this.props; @@ -90,6 +91,12 @@ class Settings extends Component { <div className="col-md-12"> <Card bodyType="card-body box-body--settings"> <div className="form"> + <FiltersConfig + interval={filtering.interval} + enabled={filtering.enabled} + processing={filtering.processingSetConfig} + setFiltersConfig={setFiltersConfig} + /> {this.renderSettings(settings.settingsList)} </div> </Card> @@ -134,6 +141,8 @@ Settings.propTypes = { getStatsConfig: PropTypes.func.isRequired, setStatsConfig: PropTypes.func.isRequired, resetStats: PropTypes.func.isRequired, + setFiltersConfig: PropTypes.func.isRequired, + getFilteringStatus: PropTypes.func.isRequired, t: PropTypes.func.isRequired, }; diff --git a/client/src/components/ui/CellWrap.js b/client/src/components/ui/CellWrap.js new file mode 100644 index 00000000..93904fa5 --- /dev/null +++ b/client/src/components/ui/CellWrap.js @@ -0,0 +1,19 @@ +import React from 'react'; +import PropTypes from 'prop-types'; + +const CellWrap = ({ value }) => ( + <div className="logs__row logs__row--overflow"> + <span className="logs__text logs__text--full" title={value}> + {value} + </span> + </div> +); + +CellWrap.propTypes = { + value: PropTypes.oneOfType([ + PropTypes.string, + PropTypes.number, + ]), +}; + +export default CellWrap; diff --git a/client/src/components/ui/Checkbox.css b/client/src/components/ui/Checkbox.css index a39f70ec..ff657898 100644 --- a/client/src/components/ui/Checkbox.css +++ b/client/src/components/ui/Checkbox.css @@ -18,7 +18,7 @@ } .checkbox--settings .checkbox__label-title { - margin-bottom: 5px; + margin-bottom: 2px; font-weight: 600; } @@ -53,7 +53,7 @@ background-position: center center; background-size: 12px 10px; border-radius: 3px; - transition: 0.3s ease box-shadow; + transition: 0.3s ease-in-out box-shadow, 0.3s ease-in-out opacity; } .checkbox__label .checkbox__label-text { @@ -82,10 +82,13 @@ } .checkbox__input:disabled + .checkbox__label { - opacity: 0.6; cursor: default; } +.checkbox__input:disabled + .checkbox__label:before { + opacity: 0.6; +} + .checkbox__label-text { max-width: 515px; line-height: 1.5; diff --git a/client/src/components/ui/Checkbox.js b/client/src/components/ui/Checkbox.js index 79051457..3cb8aab6 100644 --- a/client/src/components/ui/Checkbox.js +++ b/client/src/components/ui/Checkbox.js @@ -14,7 +14,7 @@ class Checkbox extends Component { t, } = this.props; return ( - <div className="form__group"> + <div className="form__group form__group--checkbox"> <label className="checkbox checkbox--settings"> <span className="checkbox__marker"/> <input type="checkbox" className="checkbox__input" onChange={handleChange} checked={enabled}/> diff --git a/client/src/containers/Filters.js b/client/src/containers/Filters.js index 21e56f20..134c036c 100644 --- a/client/src/containers/Filters.js +++ b/client/src/containers/Filters.js @@ -1,5 +1,14 @@ import { connect } from 'react-redux'; -import * as actionCreators from '../actions'; +import { + setRules, + getFilteringStatus, + addFilter, + removeFilter, + toggleFilterStatus, + toggleFilteringModal, + refreshFilters, + handleRulesChange, +} from '../actions/filtering'; import Filters from '../components/Filters'; const mapStateToProps = (state) => { @@ -8,7 +17,18 @@ const mapStateToProps = (state) => { return props; }; +const mapDispatchToProps = { + setRules, + getFilteringStatus, + addFilter, + removeFilter, + toggleFilterStatus, + toggleFilteringModal, + refreshFilters, + handleRulesChange, +}; + export default connect( mapStateToProps, - actionCreators, + mapDispatchToProps, )(Filters); diff --git a/client/src/containers/Logs.js b/client/src/containers/Logs.js index b2e4c56d..c512f495 100644 --- a/client/src/containers/Logs.js +++ b/client/src/containers/Logs.js @@ -1,5 +1,6 @@ import { connect } from 'react-redux'; -import { getFilteringStatus, setRules, addSuccessToast, getClients } from '../actions'; +import { addSuccessToast, getClients } from '../actions'; +import { getFilteringStatus, setRules } from '../actions/filtering'; import { getLogs, getLogsConfig } from '../actions/queryLogs'; import Logs from '../components/Logs'; diff --git a/client/src/containers/Settings.js b/client/src/containers/Settings.js index b78e140d..866765de 100644 --- a/client/src/containers/Settings.js +++ b/client/src/containers/Settings.js @@ -3,17 +3,19 @@ import { initSettings, toggleSetting } from '../actions'; import { getBlockedServices, setBlockedServices } from '../actions/services'; import { getStatsConfig, setStatsConfig, resetStats } from '../actions/stats'; import { clearLogs, getLogsConfig, setLogsConfig } from '../actions/queryLogs'; +import { getFilteringStatus, setFiltersConfig } from '../actions/filtering'; import Settings from '../components/Settings'; const mapStateToProps = (state) => { const { - settings, services, stats, queryLogs, + settings, services, stats, queryLogs, filtering, } = state; const props = { settings, services, stats, queryLogs, + filtering, }; return props; }; @@ -29,6 +31,8 @@ const mapDispatchToProps = { clearLogs, getLogsConfig, setLogsConfig, + getFilteringStatus, + setFiltersConfig, }; export default connect( diff --git a/client/src/helpers/constants.js b/client/src/helpers/constants.js index 6001293a..997002fd 100644 --- a/client/src/helpers/constants.js +++ b/client/src/helpers/constants.js @@ -265,3 +265,5 @@ export const FILTERED_STATUS = { export const STATS_INTERVALS_DAYS = [1, 7, 30, 90]; export const QUERY_LOG_INTERVALS_DAYS = [1, 7, 30, 90]; + +export const FILTERS_INTERVALS_HOURS = [0, 1, 12, 24, 72, 168]; diff --git a/client/src/helpers/form.js b/client/src/helpers/form.js index e3b6e46c..cac9882e 100644 --- a/client/src/helpers/form.js +++ b/client/src/helpers/form.js @@ -32,28 +32,36 @@ export const renderRadioField = ({ }) => ( <Fragment> <label className="custom-control custom-radio custom-control-inline"> - <input - {...input} - type="radio" - className="custom-control-input" - disabled={disabled} - /> + <input {...input} type="radio" className="custom-control-input" disabled={disabled} /> <span className="custom-control-label">{placeholder}</span> </label> - {!disabled && touched && (error && <span className="form__message form__message--error">{error}</span>)} + {!disabled && + touched && + (error && <span className="form__message form__message--error">{error}</span>)} </Fragment> ); export const renderSelectField = ({ - input, placeholder, disabled, meta: { touched, error }, + input, + placeholder, + subtitle, + disabled, + modifier = 'checkbox--form', + meta: { touched, error }, }) => ( <Fragment> - <label className="checkbox checkbox--form"> + <label className={`checkbox ${modifier}`}> <span className="checkbox__marker" /> <input {...input} type="checkbox" className="checkbox__input" disabled={disabled} /> <span className="checkbox__label"> <span className="checkbox__label-text checkbox__label-text--long"> <span className="checkbox__label-title">{placeholder}</span> + {subtitle && ( + <span + className="checkbox__label-subtitle" + dangerouslySetInnerHTML={{ __html: subtitle }} + /> + )} </span> </span> </label> @@ -64,7 +72,12 @@ export const renderSelectField = ({ ); export const renderServiceField = ({ - input, placeholder, disabled, modifier, icon, meta: { touched, error }, + input, + placeholder, + disabled, + modifier, + icon, + meta: { touched, error }, }) => ( <Fragment> <label className={`service custom-switch ${modifier}`}> @@ -81,7 +94,9 @@ export const renderServiceField = ({ <use xlinkHref={`#${icon}`} /> </svg> </label> - {!disabled && touched && (error && <span className="form__message form__message--error">{error}</span>)} + {!disabled && + touched && + (error && <span className="form__message form__message--error">{error}</span>)} </Fragment> ); diff --git a/client/src/helpers/helpers.js b/client/src/helpers/helpers.js index ad0e94dd..7f2ab78d 100644 --- a/client/src/helpers/helpers.js +++ b/client/src/helpers/helpers.js @@ -6,6 +6,7 @@ import addDays from 'date-fns/add_days'; import subDays from 'date-fns/sub_days'; import round from 'lodash/round'; import axios from 'axios'; +import i18n from 'i18next'; import { STANDARD_DNS_PORT, @@ -19,6 +20,21 @@ export const formatTime = (time) => { return dateFormat(parsedTime, 'HH:mm:ss'); }; +export const formatDateTime = (dateTime) => { + const currentLanguage = i18n.languages[0] || 'en'; + const parsedTime = dateParse(dateTime); + const options = { + year: 'numeric', + month: 'numeric', + day: 'numeric', + hour: 'numeric', + minute: 'numeric', + hour12: false, + }; + + return parsedTime.toLocaleString(currentLanguage, options); +}; + export const normalizeLogs = logs => logs.map((log) => { const { time, @@ -74,18 +90,38 @@ export const normalizeTopStats = stats => ( ); export const normalizeFilteringStatus = (filteringStatus) => { - const { enabled, filters, user_rules: userRules } = filteringStatus; - const newFilters = filters ? filters.map((filter) => { - const { - id, url, enabled, lastUpdated: lastUpdated = Date.now(), name = 'Default name', rulesCount: rulesCount = 0, - } = filter; + const { + enabled, filters, user_rules: userRules, interval, + } = filteringStatus; + const newFilters = filters + ? filters.map((filter) => { + const { + id, + url, + enabled, + last_updated, + name = 'Default name', + rules_count: rules_count = 0, + } = filter; - return { - id, url, enabled, lastUpdated: formatTime(lastUpdated), name, rulesCount, - }; - }) : []; + return { + id, + url, + enabled, + lastUpdated: last_updated ? formatDateTime(last_updated) : '–', + name, + rulesCount: rules_count, + }; + }) + : []; const newUserRules = Array.isArray(userRules) ? userRules.join('\n') : ''; - return { enabled, userRules: newUserRules, filters: newFilters }; + + return { + enabled, + userRules: newUserRules, + filters: newFilters, + interval, + }; }; export const getPercent = (amount, number) => { @@ -241,3 +277,5 @@ export const secondsToMilliseconds = (seconds) => { return seconds; }; + +export const normalizeRulesTextarea = text => text && text.replace(/^\n/g, '').replace(/\n\s*\n/g, '\n'); diff --git a/client/src/reducers/filtering.js b/client/src/reducers/filtering.js new file mode 100644 index 00000000..4ed0acd8 --- /dev/null +++ b/client/src/reducers/filtering.js @@ -0,0 +1,86 @@ +import { handleActions } from 'redux-actions'; + +import * as actions from '../actions/filtering'; + +const filtering = handleActions( + { + [actions.setRulesRequest]: state => ({ ...state, processingRules: true }), + [actions.setRulesFailure]: state => ({ ...state, processingRules: false }), + [actions.setRulesSuccess]: state => ({ ...state, processingRules: false }), + + [actions.handleRulesChange]: (state, { payload }) => { + const { userRules } = payload; + return { ...state, userRules }; + }, + + [actions.getFilteringStatusRequest]: state => ({ ...state, processingFilters: true }), + [actions.getFilteringStatusFailure]: state => ({ ...state, processingFilters: false }), + [actions.getFilteringStatusSuccess]: (state, { payload }) => ({ + ...state, + ...payload, + processingFilters: false, + }), + + [actions.addFilterRequest]: state => ({ + ...state, + processingAddFilter: true, + isFilterAdded: false, + }), + [actions.addFilterFailure]: state => ({ + ...state, + processingAddFilter: false, + isFilterAdded: false, + }), + [actions.addFilterSuccess]: state => ({ + ...state, + processingAddFilter: false, + isFilterAdded: true, + }), + + [actions.toggleFilteringModal]: (state) => { + const newState = { + ...state, + isModalOpen: !state.isModalOpen, + isFilterAdded: false, + }; + return newState; + }, + + [actions.toggleFilterRequest]: state => ({ ...state, processingConfigFilter: true }), + [actions.toggleFilterFailure]: state => ({ ...state, processingConfigFilter: false }), + [actions.toggleFilterSuccess]: state => ({ ...state, processingConfigFilter: false }), + + [actions.refreshFiltersRequest]: state => ({ ...state, processingRefreshFilters: true }), + [actions.refreshFiltersFailure]: state => ({ ...state, processingRefreshFilters: false }), + [actions.refreshFiltersSuccess]: state => ({ ...state, processingRefreshFilters: false }), + + [actions.removeFilterRequest]: state => ({ ...state, processingRemoveFilter: true }), + [actions.removeFilterFailure]: state => ({ ...state, processingRemoveFilter: false }), + [actions.removeFilterSuccess]: state => ({ ...state, processingRemoveFilter: false }), + + [actions.setFiltersConfigRequest]: state => ({ ...state, processingSetConfig: true }), + [actions.setFiltersConfigFailure]: state => ({ ...state, processingSetConfig: false }), + [actions.setFiltersConfigSuccess]: (state, { payload }) => ({ + ...state, + ...payload, + processingSetConfig: false, + }), + }, + { + isModalOpen: false, + processingFilters: false, + processingRules: false, + processingAddFilter: false, + processingRefreshFilters: false, + processingConfigFilter: false, + processingRemoveFilter: false, + processingSetConfig: false, + isFilterAdded: false, + filters: [], + userRules: '', + interval: 24, + enabled: true, + }, +); + +export default filtering; diff --git a/client/src/reducers/index.js b/client/src/reducers/index.js index 49e7729f..9339f042 100644 --- a/client/src/reducers/index.js +++ b/client/src/reducers/index.js @@ -13,6 +13,7 @@ import rewrites from './rewrites'; import services from './services'; import stats from './stats'; import queryLogs from './queryLogs'; +import filtering from './filtering'; const settings = handleActions({ [actions.initSettingsRequest]: state => ({ ...state, processing: true }), @@ -130,13 +131,6 @@ const dashboard = handleActions({ return newState; }, - [actions.getFilteringRequest]: state => ({ ...state, processingFiltering: true }), - [actions.getFilteringFailure]: state => ({ ...state, processingFiltering: false }), - [actions.getFilteringSuccess]: (state, { payload }) => { - const newState = { ...state, isFilteringEnabled: payload, processingFiltering: false }; - return newState; - }, - [actions.toggleProtectionRequest]: state => ({ ...state, processingProtection: true }), [actions.toggleProtectionFailure]: state => ({ ...state, processingProtection: false }), [actions.toggleProtectionSuccess]: (state) => { @@ -189,62 +183,6 @@ const dashboard = handleActions({ autoClients: [], }); -const filtering = handleActions({ - [actions.setRulesRequest]: state => ({ ...state, processingRules: true }), - [actions.setRulesFailure]: state => ({ ...state, processingRules: false }), - [actions.setRulesSuccess]: state => ({ ...state, processingRules: false }), - - [actions.handleRulesChange]: (state, { payload }) => { - const { userRules } = payload; - return { ...state, userRules }; - }, - - [actions.getFilteringStatusRequest]: state => ({ ...state, processingFilters: true }), - [actions.getFilteringStatusFailure]: state => ({ ...state, processingFilters: false }), - [actions.getFilteringStatusSuccess]: (state, { payload }) => { - const { status } = payload; - const { filters, userRules } = status; - const newState = { - ...state, filters, userRules, processingFilters: false, - }; - return newState; - }, - - [actions.addFilterRequest]: state => - ({ ...state, processingAddFilter: true, isFilterAdded: false }), - [actions.addFilterFailure]: (state) => { - const newState = { ...state, processingAddFilter: false, isFilterAdded: false }; - return newState; - }, - [actions.addFilterSuccess]: state => - ({ ...state, processingAddFilter: false, isFilterAdded: true }), - - [actions.toggleFilteringModal]: (state) => { - const newState = { - ...state, - isFilteringModalOpen: !state.isFilteringModalOpen, - isFilterAdded: false, - }; - return newState; - }, - - [actions.toggleFilterRequest]: state => ({ ...state, processingFilters: true }), - [actions.toggleFilterFailure]: state => ({ ...state, processingFilters: false }), - [actions.toggleFilterSuccess]: state => ({ ...state, processingFilters: false }), - - [actions.refreshFiltersRequest]: state => ({ ...state, processingRefreshFilters: true }), - [actions.refreshFiltersFailure]: state => ({ ...state, processingRefreshFilters: false }), - [actions.refreshFiltersSuccess]: state => ({ ...state, processingRefreshFilters: false }), -}, { - isFilteringModalOpen: false, - processingFilters: false, - processingRules: false, - processingAddFilter: false, - processingRefreshFilters: false, - filters: [], - userRules: '', -}); - const dhcp = handleActions({ [actions.getDhcpStatusRequest]: state => ({ ...state, processing: true }), [actions.getDhcpStatusFailure]: state => ({ ...state, processing: false }),