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()}
+ }
+