diff --git a/AGHTechDoc.md b/AGHTechDoc.md index 8f69e5bd..c1b53300 100644 --- a/AGHTechDoc.md +++ b/AGHTechDoc.md @@ -399,6 +399,7 @@ Response: "protection_enabled":true, "running":true, "dhcp_available":true, + "protection_disabled_duration":0 "version":"undefined" } diff --git a/CHANGELOG.md b/CHANGELOG.md index a8f44577..d14a6f5f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -25,6 +25,11 @@ NOTE: Add new changes BELOW THIS COMMENT. ### Added +- The new HTTP API `POST /control/protection`, that updates protection state + and adds an optional pause duration ([#1333]). The format of request body + is described in `openapi/openapi.yaml`. The duration of this pause could + also be set with the config field `protection_disabled_until` in `dns` + section of the YAML configuration file. - Two new HTTP APIs, `PUT /control/stats/config/update` and `GET control/stats/config`, which can be used to set and receive the query log configuration. See openapi/openapi.yaml for the full description. @@ -122,6 +127,7 @@ In this release, the schema version has changed from 17 to 20. ([#5584]). [#1163]: https://github.com/AdguardTeam/AdGuardHome/issues/1163 +[#1333]: https://github.com/AdguardTeam/AdGuardHome/issues/1333 [#1472]: https://github.com/AdguardTeam/AdGuardHome/issues/1472 [#5567]: https://github.com/AdguardTeam/AdGuardHome/issues/5567 [#5584]: https://github.com/AdguardTeam/AdGuardHome/issues/5584 diff --git a/client/src/__locales/en.json b/client/src/__locales/en.json index 08f3c08f..2448b80f 100644 --- a/client/src/__locales/en.json +++ b/client/src/__locales/en.json @@ -650,5 +650,20 @@ "confirm_dns_cache_clear": "Are you sure you want to clear DNS cache?", "cache_cleared": "DNS cache successfully cleared", "clear_cache": "Clear cache", - "make_static": "Make static" + "make_static": "Make static", + "disable_for_seconds": "For {{count}} second", + "disable_for_seconds_plural": "For {{count}} seconds", + "disable_for_minutes": "For {{count}} minute", + "disable_for_minutes_plural": "For {{count}} minutes", + "disable_for_hours": "For {{count}} hour", + "disable_for_hours_plural": "For {{count}} hours", + "disable_until_tomorrow": "Until tomorrow", + "disable_notify_for_seconds": "Disable protection for {{count}} second", + "disable_notify_for_seconds_plural": "Disable protection for {{count}} seconds", + "disable_notify_for_minutes": "Disable protection for {{count}} minute", + "disable_notify_for_minutes_plural": "Disable protection for {{count}} minutes", + "disable_notify_for_hours": "Disable protection for {{count}} hour", + "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}}" } diff --git a/client/src/actions/index.js b/client/src/actions/index.js index a164f51a..5d96b045 100644 --- a/client/src/actions/index.js +++ b/client/src/actions/index.js @@ -6,7 +6,14 @@ import endsWith from 'lodash/endsWith'; import escapeRegExp from 'lodash/escapeRegExp'; import React from 'react'; import { compose } from 'redux'; -import { splitByNewLine, sortClients, filterOutComments } from '../helpers/helpers'; +import { + splitByNewLine, + sortClients, + filterOutComments, + msToSeconds, + msToMinutes, + msToHours, +} from '../helpers/helpers'; import { BLOCK_ACTIONS, CHECK_TIMEOUT, @@ -14,6 +21,7 @@ import { SETTINGS_NAMES, FORM_NAME, MANUAL_UPDATE_LINK, + DISABLE_PROTECTION_TIMINGS, } from '../helpers/constants'; import { areEqualVersions } from '../helpers/version'; import { getTlsStatus } from './encryption'; @@ -108,19 +116,54 @@ export const toggleProtectionRequest = createAction('TOGGLE_PROTECTION_REQUEST') export const toggleProtectionFailure = createAction('TOGGLE_PROTECTION_FAILURE'); export const toggleProtectionSuccess = createAction('TOGGLE_PROTECTION_SUCCESS'); -export const toggleProtection = (status) => async (dispatch) => { +const getDisabledMessage = (time) => { + switch (time) { + case DISABLE_PROTECTION_TIMINGS.HALF_MINUTE: + return i18next.t( + 'disable_notify_for_seconds', + { count: msToSeconds(DISABLE_PROTECTION_TIMINGS.HALF_MINUTE) }, + ); + case DISABLE_PROTECTION_TIMINGS.MINUTE: + return i18next.t( + 'disable_notify_for_minutes', + { count: msToMinutes(DISABLE_PROTECTION_TIMINGS.MINUTE) }, + ); + case DISABLE_PROTECTION_TIMINGS.TEN_MINUTES: + return i18next.t( + 'disable_notify_for_minutes', + { count: msToMinutes(DISABLE_PROTECTION_TIMINGS.TEN_MINUTES) }, + ); + case DISABLE_PROTECTION_TIMINGS.HOUR: + return i18next.t( + 'disable_notify_for_hours', + { count: msToHours(DISABLE_PROTECTION_TIMINGS.HOUR) }, + ); + case DISABLE_PROTECTION_TIMINGS.TOMORROW: + return i18next.t('disable_notify_until_tomorrow'); + default: + return 'disabled_protection'; + } +}; + +export const toggleProtection = (status, time = null) => async (dispatch) => { dispatch(toggleProtectionRequest()); try { - const successMessage = status ? 'disabled_protection' : 'enabled_protection'; - await apiClient.setDnsConfig({ protection_enabled: !status }); + const successMessage = status ? getDisabledMessage(time) : 'enabled_protection'; + await apiClient.setProtection({ enabled: !status, duration: time }); dispatch(addSuccessToast(successMessage)); - dispatch(toggleProtectionSuccess()); + dispatch(toggleProtectionSuccess({ disabledDuration: time })); } catch (error) { dispatch(addErrorToast({ error })); dispatch(toggleProtectionFailure()); } }; +export const setDisableDurationTime = createAction('SET_DISABLED_DURATION_TIME'); + +export const setProtectionTimerTime = (updatedTime) => async (dispatch) => { + dispatch(setDisableDurationTime({ timeToEnableProtection: updatedTime })); +}; + export const getVersionRequest = createAction('GET_VERSION_REQUEST'); export const getVersionFailure = createAction('GET_VERSION_FAILURE'); export const getVersionSuccess = createAction('GET_VERSION_SUCCESS'); @@ -273,6 +316,9 @@ export const getDnsStatus = () => async (dispatch) => { const handleRequestSuccess = (response) => { const dnsStatus = response.data; + if (dnsStatus.protection_disabled_duration === 0) { + dnsStatus.protection_disabled_duration = null; + } const { running } = dnsStatus; const runningStatus = dnsStatus && running; if (runningStatus === true) { diff --git a/client/src/api/Api.js b/client/src/api/Api.js index caf836b8..7ca33293 100644 --- a/client/src/api/Api.js +++ b/client/src/api/Api.js @@ -627,6 +627,15 @@ class Api { return this.makeRequest(path, method, config); } + SET_PROTECTION = { path: 'protection', method: 'POST' }; + + setProtection(data) { + const { enabled, duration } = data; + const { path, method } = this.SET_PROTECTION; + + return this.makeRequest(path, method, { data: { enabled, duration } }); + } + // Cache CLEAR_CACHE = { path: 'cache_clear', method: 'POST' }; diff --git a/client/src/components/App/index.js b/client/src/components/App/index.js index 819bb0c6..3d2db100 100644 --- a/client/src/components/App/index.js +++ b/client/src/components/App/index.js @@ -43,6 +43,7 @@ import DnsRewrites from '../../containers/DnsRewrites'; import CustomRules from '../../containers/CustomRules'; import Services from '../Filters/Services'; import Logs from '../Logs'; +import ProtectionTimer from '../ProtectionTimer'; const ROUTES = [ { @@ -191,6 +192,7 @@ const App = () => { {!processingEncryption && }
+
{processing && } {!isCoreRunning &&
diff --git a/client/src/components/Dashboard/Dashboard.css b/client/src/components/Dashboard/Dashboard.css index 415a3f6b..765c9ed1 100644 --- a/client/src/components/Dashboard/Dashboard.css +++ b/client/src/components/Dashboard/Dashboard.css @@ -1,3 +1,9 @@ +.dashboard-protection-button.btn-gray { + border-top-right-radius: 0; + border-bottom-right-radius: 0; + border-right-color: #a4a4a4; +} + .stats__table .popover__body { left: -10px; min-width: 270px; @@ -34,20 +40,11 @@ align-items: center; } -.dashboard-title__button { - margin: 0 0.5rem; -} - @media (max-width: 767.98px) { .page-title--dashboard { flex-direction: column; align-items: flex-start; } - - .dashboard-title__button { - margin: 0.5rem 0; - display: block; - } } .counters__row { diff --git a/client/src/components/Dashboard/index.js b/client/src/components/Dashboard/index.js index cc19acf0..ab8d4efc 100644 --- a/client/src/components/Dashboard/index.js +++ b/client/src/components/Dashboard/index.js @@ -9,18 +9,20 @@ import Counters from './Counters'; import Clients from './Clients'; import QueriedDomains from './QueriedDomains'; import BlockedDomains from './BlockedDomains'; -import { SETTINGS_URLS } from '../../helpers/constants'; +import { DISABLE_PROTECTION_TIMINGS, ONE_SECOND_IN_MS, SETTINGS_URLS } from '../../helpers/constants'; +import { msToSeconds, msToMinutes, msToHours } from '../../helpers/helpers'; import PageTitle from '../ui/PageTitle'; import Loading from '../ui/Loading'; import './Dashboard.css'; +import Dropdown from '../ui/Dropdown'; const Dashboard = ({ getAccessList, getStats, getStatsConfig, dashboard, - dashboard: { protectionEnabled, processingProtection }, + dashboard: { protectionEnabled, processingProtection, protectionDisabledDuration }, toggleProtection, stats, access, @@ -36,7 +38,6 @@ const Dashboard = ({ useEffect(() => { getAllStats(); }, []); - const getSubtitle = () => { if (stats.interval === 0) { return t('stats_disabled_short'); @@ -47,9 +48,7 @@ const Dashboard = ({ : t('for_last_days', { count: stats.interval }); }; - const buttonText = protectionEnabled ? 'disable_protection' : 'enable_protection'; - - const buttonClass = classNames('btn btn-sm dashboard-title__button', { + const buttonClass = classNames('btn btn-sm dashboard-protection-button', { 'btn-gray': protectionEnabled, 'btn-success': !protectionEnabled, }); @@ -71,16 +70,87 @@ const Dashboard = ({ const subtitle = getSubtitle(); + const DISABLE_PROTECTION_ITEMS = [ + { + text: t('disable_for_seconds', { count: msToSeconds(DISABLE_PROTECTION_TIMINGS.HALF_MINUTE) }), + disableTime: DISABLE_PROTECTION_TIMINGS.HALF_MINUTE, + }, + { + text: t('disable_for_minutes', { count: msToMinutes(DISABLE_PROTECTION_TIMINGS.MINUTE) }), + disableTime: DISABLE_PROTECTION_TIMINGS.MINUTE, + }, + { + text: t('disable_for_minutes', { count: msToMinutes(DISABLE_PROTECTION_TIMINGS.TEN_MINUTES) }), + disableTime: DISABLE_PROTECTION_TIMINGS.TEN_MINUTES, + }, + { + text: t('disable_for_hours', { count: msToHours(DISABLE_PROTECTION_TIMINGS.HOUR) }), + disableTime: DISABLE_PROTECTION_TIMINGS.HOUR, + }, + { + text: t('disable_until_tomorrow'), + disableTime: DISABLE_PROTECTION_TIMINGS.TOMORROW, + }, + ]; + + const getDisableProtectionItems = () => ( + Object.values(DISABLE_PROTECTION_ITEMS) + .map((item, index) => ( +
{ + toggleProtection(protectionEnabled, item.disableTime - ONE_SECOND_IN_MS); + }} + > + {item.text} +
+ )) + ); + + const getRemaningTimeText = (milliseconds) => { + if (!milliseconds) { + return ''; + } + + const date = new Date(milliseconds); + const hh = date.getUTCHours(); + const mm = `0${date.getUTCMinutes()}`.slice(-2); + const ss = `0${date.getUTCSeconds()}`.slice(-2); + const formattedHH = `0${hh}`.slice(-2); + + return hh ? `${formattedHH}:${mm}:${ss}` : `${mm}:${ss}`; + }; + + const getProtectionBtnText = (status) => (status ? t('disable_protection') : t('enable_protection')); + return <> - +
+ + + {protectionEnabled && + {getDisableProtectionItems()} + } +