Pull request 1907: 951-blocked-services-schedule-api

Updates #951.

Squashed commit of the following:

commit 6b840fd516f5a87fde0420e3aceb9c239b22c974
Author: Stanislav Chzhen <s.chzhen@adguard.com>
Date:   Tue Aug 29 19:53:03 2023 +0300

    client: imp docs more

commit 7fc8f0363fbe4c4266cb0f67428fe4d18c351d2d
Author: Stanislav Chzhen <s.chzhen@adguard.com>
Date:   Tue Aug 29 19:40:00 2023 +0300

    client: imp docs

commit 00bc14d5760614f2797714cdc2c4c19b1a94b86e
Author: Ildar Kamalov <ik@adguard.com>
Date:   Mon Aug 28 18:43:49 2023 +0300

    try to fix lock file

commit d749df74b576091e0b58928d86ea8b3b49f919da
Merge: c69f9230b e1f6229e5
Author: Stanislav Chzhen <s.chzhen@adguard.com>
Date:   Mon Aug 28 18:14:02 2023 +0300

    Merge branch 'master' into 951-blocked-services-schedule-api

commit c69f9230b12f7c983db06b74324b3df77d74b32b
Author: Ildar Kamalov <ik@adguard.com>
Date:   Mon Aug 28 17:16:20 2023 +0300

    revert eslintrc

commit b37916c2dff0ddea5293d87570bb58e3443d2d21
Author: Ildar Kamalov <ik@adguard.com>
Date:   Mon Aug 28 12:02:39 2023 +0300

    fix translations

commit f5bb67d81506c687d0abd580049a3eee0af808e0
Author: Ildar Kamalov <ik@adguard.com>
Date:   Mon Aug 28 11:43:57 2023 +0300

    fix helpers

commit 13ec6a8b3a0acfb62762ae7e46c6e98eb7c82212
Author: Ildar Kamalov <ik@adguard.com>
Date:   Mon Aug 28 11:24:57 2023 +0300

    remove todo

commit 23724ec2fd683ed17b9f1cee841ad9aaf4c9d04f
Author: Ildar Kamalov <ik@adguard.com>
Date:   Mon Aug 28 09:56:56 2023 +0300

    add clients schedule form

commit 84d29e558a329068e64e7a95ee183946aa4515b5
Author: Ildar Kamalov <ik@adguard.com>
Date:   Fri Aug 25 17:44:40 2023 +0300

    fix schedule form

commit 83e4017688082e9eb670091d5a24d98157050502
Author: Ildar Kamalov <ik@adguard.com>
Date:   Fri Aug 18 12:58:16 2023 +0300

    remove unused

commit ef2b68e138da382e3cf42586ae604e12d9493504
Author: Ildar Kamalov <ik@adguard.com>
Date:   Fri Aug 18 12:57:37 2023 +0300

    client: fix translation string

commit 32ea80c968f52f18adbc811b2f06874644cdfe20
Author: Ildar Kamalov <ik@adguard.com>
Date:   Fri Aug 18 12:26:26 2023 +0300

    wip schedule

commit 9b770873859186c9424c8d108812e32ddff33bad
Author: Stanislav Chzhen <s.chzhen@adguard.com>
Date:   Fri Jul 21 14:29:50 2023 +0300

    all: imp naming

commit ea4e9514ea3b264bcce7f2a301db817de4e87059
Author: Stanislav Chzhen <s.chzhen@adguard.com>
Date:   Wed Jul 19 18:09:27 2023 +0300

    all: imp code

commit 98a705bdaa5c1e79394c73e5d75af2416fe9f297
Author: Stanislav Chzhen <s.chzhen@adguard.com>
Date:   Tue Jul 18 18:23:26 2023 +0300

    all: imp naming

commit 4f84b55c7bfc9f7b680feac0ec45f5ea9189299a
Author: Stanislav Chzhen <s.chzhen@adguard.com>
Date:   Fri Jul 14 15:01:17 2023 +0300

    all: add global schedule api

commit 87cf1646869ee9138964b47a27b7493674c8854a
Merge: cabb80ac1 2adc8624c
Author: Stanislav Chzhen <s.chzhen@adguard.com>
Date:   Fri Jul 14 12:09:29 2023 +0300

    Merge branch 'master' into 951-blocked-services-schedule-api

commit cabb80ac16de437a8118bb0166479574379c97a3
Author: Stanislav Chzhen <s.chzhen@adguard.com>
Date:   Thu Jul 13 13:37:23 2023 +0300

    openapi: fix typo

commit 2279b03acbcfc3d76216f8aaf30ae1c7894127bc
Author: Stanislav Chzhen <s.chzhen@adguard.com>
Date:   Thu Jul 13 12:26:19 2023 +0300

    all: imp docs

... and 3 more commits
This commit is contained in:
Stanislav Chzhen
2023-08-29 20:03:40 +03:00
parent e1f6229e56
commit aac36a2d2f
54 changed files with 1506 additions and 209 deletions

15
client/.eslintrc.json vendored
View File

@@ -81,6 +81,19 @@
}
],
"import/prefer-default-export": "off",
"no-alert": "off"
"no-alert": "off",
"arrow-body-style": "off",
"max-len": [
"error",
120,
2,
{
"ignoreUrls": true,
"ignoreComments": false,
"ignoreRegExpLiterals": true,
"ignoreStrings": true,
"ignoreTemplateLiterals": true
}
]
}
}

2
client/dev.eslintrc vendored
View File

@@ -1,6 +1,6 @@
{
"extends": ".eslintrc",
"rules": {
"no-debugger":"warn",
"no-debugger":"warn"
}
}

5
client/package-lock.json generated vendored
View File

@@ -15094,6 +15094,11 @@
"setimmediate": "^1.0.4"
}
},
"timezones-list": {
"version": "3.0.2",
"resolved": "https://registry.npmjs.org/timezones-list/-/timezones-list-3.0.2.tgz",
"integrity": "sha512-I698hm6Jp/xxkwyTSOr39pZkYKETL8LDJeSIhjxXBfPUAHM5oZNuQ4o9UK3PSkDBOkjATecSOBb3pR1IkIBUsg=="
},
"tiny-invariant": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/tiny-invariant/-/tiny-invariant-1.1.0.tgz",

1
client/package.json vendored
View File

@@ -43,6 +43,7 @@
"redux-form": "^8.3.5",
"redux-thunk": "^2.3.0",
"string-length": "^5.0.1",
"timezones-list": "^3.0.2",
"url-polyfill": "^1.1.9"
},
"devDependencies": {

View File

@@ -680,5 +680,37 @@
"protection_section_label": "Protection",
"log_and_stats_section_label": "Query log and statistics",
"ignore_query_log": "Ignore this client in query log",
"ignore_statistics": "Ignore this client in statistics"
"ignore_statistics": "Ignore this client in statistics",
"schedule_services": "Pause service blocking",
"schedule_services_desc": "Configure the pause schedule of the service-blocking filter",
"schedule_services_desc_client": "Configure the pause schedule of the service-blocking filter for this client",
"schedule_desc": "Set inactivity periods for blocked services",
"schedule_invalid_select": "Start time must be before end time",
"schedule_select_days": "Select days",
"schedule_timezone": "Select a time zone",
"schedule_current_timezone": "Current time zone: {{value}}",
"schedule_time_all_day": "All day",
"schedule_modal_description": "This schedule will replace any existing schedules for the same day of the week. Each day of the week can have only one inactivity period.",
"schedule_modal_time_off": "No service blocking:",
"schedule_new": "New schedule",
"schedule_edit": "Edit schedule",
"schedule_save": "Save schedule",
"schedule_add": "Add schedule",
"schedule_remove": "Remove schedule",
"schedule_from": "From",
"schedule_to": "To",
"sunday": "Sunday",
"monday": "Monday",
"tuesday": "Tuesday",
"wednesday": "Wednesday",
"thursday": "Thursday",
"friday": "Friday",
"saturday": "Saturday",
"sunday_short": "Sun",
"monday_short": "Mon",
"tuesday_short": "Tue",
"wednesday_short": "Wed",
"thursday_short": "Thu",
"friday_short": "Fri",
"saturday_short": "Sat"
}

View File

@@ -32,19 +32,19 @@ export const getAllBlockedServices = () => async (dispatch) => {
}
};
export const setBlockedServicesRequest = createAction('SET_BLOCKED_SERVICES_REQUEST');
export const setBlockedServicesFailure = createAction('SET_BLOCKED_SERVICES_FAILURE');
export const setBlockedServicesSuccess = createAction('SET_BLOCKED_SERVICES_SUCCESS');
export const updateBlockedServicesRequest = createAction('UPDATE_BLOCKED_SERVICES_REQUEST');
export const updateBlockedServicesFailure = createAction('UPDATE_BLOCKED_SERVICES_FAILURE');
export const updateBlockedServicesSuccess = createAction('UPDATE_BLOCKED_SERVICES_SUCCESS');
export const setBlockedServices = (values) => async (dispatch) => {
dispatch(setBlockedServicesRequest());
export const updateBlockedServices = (values) => async (dispatch) => {
dispatch(updateBlockedServicesRequest());
try {
await apiClient.setBlockedServices(values);
dispatch(setBlockedServicesSuccess());
await apiClient.updateBlockedServices(values);
dispatch(updateBlockedServicesSuccess());
dispatch(getBlockedServices());
dispatch(addSuccessToast('blocked_services_saved'));
} catch (error) {
dispatch(addErrorToast({ error }));
dispatch(setBlockedServicesFailure());
dispatch(updateBlockedServicesFailure());
}
};

View File

@@ -489,9 +489,9 @@ class Api {
}
// Blocked services
BLOCKED_SERVICES_LIST = { path: 'blocked_services/list', method: 'GET' };
BLOCKED_SERVICES_GET = { path: 'blocked_services/get', method: 'GET' };
BLOCKED_SERVICES_SET = { path: 'blocked_services/set', method: 'POST' };
BLOCKED_SERVICES_UPDATE = { path: 'blocked_services/update', method: 'PUT' };
BLOCKED_SERVICES_ALL = { path: 'blocked_services/all', method: 'GET' };
@@ -501,12 +501,12 @@ class Api {
}
getBlockedServices() {
const { path, method } = this.BLOCKED_SERVICES_LIST;
const { path, method } = this.BLOCKED_SERVICES_GET;
return this.makeRequest(path, method);
}
setBlockedServices(config) {
const { path, method } = this.BLOCKED_SERVICES_SET;
updateBlockedServices(config) {
const { path, method } = this.BLOCKED_SERVICES_UPDATE;
const parameters = {
data: config,
};

View File

@@ -0,0 +1,220 @@
import React, { useState, useEffect } from 'react';
import PropTypes from 'prop-types';
import { useTranslation } from 'react-i18next';
import ReactModal from 'react-modal';
import { Timezone } from './Timezone';
import { TimeSelect } from './TimeSelect';
import { TimePeriod } from './TimePeriod';
import { getFullDayName, getShortDayName } from './helpers';
import { LOCAL_TIMEZONE_VALUE } from '../../../../helpers/constants';
export const DAYS_OF_WEEK = ['sun', 'mon', 'tue', 'wed', 'thu', 'fri', 'sat'];
const INITIAL_START_TIME_MS = 0;
const INITIAL_END_TIME_MS = 86340000;
export const Modal = ({
isOpen,
currentDay,
schedule,
onClose,
onSubmit,
}) => {
const [t] = useTranslation();
const intialTimezone = schedule.time_zone === LOCAL_TIMEZONE_VALUE
? Intl.DateTimeFormat().resolvedOptions().timeZone
: schedule.time_zone;
const [timezone, setTimezone] = useState(intialTimezone);
const [days, setDays] = useState(new Set());
const [startTime, setStartTime] = useState(INITIAL_START_TIME_MS);
const [endTime, setEndTime] = useState(INITIAL_END_TIME_MS);
const [wrongPeriod, setWrongPeriod] = useState(true);
useEffect(() => {
if (currentDay) {
const newDays = new Set([currentDay]);
setDays(newDays);
setStartTime(schedule[currentDay].start);
setEndTime(schedule[currentDay].end);
}
}, [currentDay]);
useEffect(() => {
if (startTime >= endTime) {
setWrongPeriod(true);
} else {
setWrongPeriod(false);
}
}, [startTime, endTime]);
const addDays = (day) => {
const newDays = new Set(days);
if (newDays.has(day)) {
newDays.delete(day);
} else {
newDays.add(day);
}
setDays(newDays);
};
const activeDay = (day) => {
return days.has(day);
};
const onFormSubmit = (e) => {
e.preventDefault();
if (currentDay) {
const newSchedule = schedule;
Array.from(days).forEach((day) => {
newSchedule[day] = {
start: startTime,
end: endTime,
};
});
onSubmit(newSchedule);
} else {
const newSchedule = {
time_zone: timezone,
};
Array.from(days).forEach((day) => {
newSchedule[day] = {
start: startTime,
end: endTime,
};
});
onSubmit(newSchedule);
}
};
return (
<ReactModal
className="Modal__Bootstrap modal-dialog modal-dialog-centered modal-dialog--schedule"
closeTimeoutMS={0}
isOpen={isOpen}
onRequestClose={onClose}
>
<div className="modal-content">
<div className="modal-header">
<h4 className="modal-title">
{currentDay ? t('schedule_edit') : t('schedule_new')}
</h4>
<button type="button" className="close" onClick={onClose}>
<span className="sr-only">Close</span>
</button>
</div>
<form onSubmit={onFormSubmit}>
<div className="modal-body">
<Timezone
timezone={timezone}
setTimezone={setTimezone}
/>
<div className="schedule__days">
{DAYS_OF_WEEK.map((day) => (
<button
type="button"
key={day}
className="btn schedule__button-day"
data-active={activeDay(day)}
onClick={() => addDays(day)}
>
{getShortDayName(t, day)}
</button>
)) }
</div>
<div className="schedule__time-wrap">
<div className="schedule__time-row">
<TimeSelect
value={startTime}
onChange={(v) => setStartTime(v)}
/>
<TimeSelect
value={endTime}
onChange={(v) => setEndTime(v)}
/>
</div>
{wrongPeriod && (
<div className="schedule__error">
{t('schedule_invalid_select')}
</div>
)}
</div>
<div className="schedule__info">
<div className="schedule__info-title">
{t('schedule_modal_time_off')}
</div>
<div className="schedule__info-row">
<svg className="icons schedule__info-icon">
<use xlinkHref="#calendar" />
</svg>
{days.size ? (
Array.from(days).map((day) => getFullDayName(t, day)).join(', ')
) : (
<span>
</span>
)}
</div>
<div className="schedule__info-row">
<svg className="icons schedule__info-icon">
<use xlinkHref="#watch" />
</svg>
{wrongPeriod ? (
<span>
</span>
) : (
<TimePeriod
startTimeMs={startTime}
endTimeMs={endTime}
/>
)}
</div>
</div>
<div className="schedule__notice">
{t('schedule_modal_description')}
</div>
</div>
<div className="modal-footer">
<div className="btn-list">
<button
type="button"
className="btn btn-success btn-standard"
disabled={days.size === 0 || wrongPeriod}
onClick={onFormSubmit}
>
{currentDay ? t('schedule_save') : t('schedule_add')}
</button>
</div>
</div>
</form>
</div>
</ReactModal>
);
};
Modal.propTypes = {
schedule: PropTypes.object.isRequired,
currentDay: PropTypes.string,
isOpen: PropTypes.bool.isRequired,
onClose: PropTypes.func.isRequired,
onSubmit: PropTypes.func.isRequired,
};

View File

@@ -0,0 +1,25 @@
import React from 'react';
import PropTypes from 'prop-types';
import { getTimeFromMs } from './helpers';
export const TimePeriod = ({
startTimeMs,
endTimeMs,
}) => {
const startTime = getTimeFromMs(startTimeMs);
const endTime = getTimeFromMs(endTimeMs);
return (
<div className="schedule__time">
<time>{startTime.hours}:{startTime.minutes}</time>
&nbsp;&nbsp;
<time>{endTime.hours}:{endTime.minutes}</time>
</div>
);
};
TimePeriod.propTypes = {
startTimeMs: PropTypes.number.isRequired,
endTimeMs: PropTypes.number.isRequired,
};

View File

@@ -0,0 +1,60 @@
import React, { useState } from 'react';
import PropTypes from 'prop-types';
import { getTimeFromMs, convertTimeToMs } from './helpers';
export const TimeSelect = ({
value,
onChange,
}) => {
const { hours: initialHours, minutes: initialMinutes } = getTimeFromMs(value);
const [hours, setHours] = useState(initialHours);
const [minutes, setMinutes] = useState(initialMinutes);
const hourOptions = Array.from({ length: 24 }, (_, i) => i.toString().padStart(2, '0'));
const minuteOptions = Array.from({ length: 60 }, (_, i) => i.toString().padStart(2, '0'));
const onHourChange = (event) => {
setHours(event.target.value);
onChange(convertTimeToMs(event.target.value, minutes));
};
const onMinuteChange = (event) => {
setMinutes(event.target.value);
onChange(convertTimeToMs(hours, event.target.value));
};
return (
<div className="schedule__time-select">
<select
value={hours}
onChange={onHourChange}
className="form-control custom-select"
>
{hourOptions.map((hour) => (
<option key={hour} value={hour}>
{hour}
</option>
))}
</select>
&nbsp;:&nbsp;
<select
value={minutes}
onChange={onMinuteChange}
className="form-control custom-select"
>
{minuteOptions.map((minute) => (
<option key={minute} value={minute}>
{minute}
</option>
))}
</select>
</div>
);
};
TimeSelect.propTypes = {
value: PropTypes.number.isRequired,
onChange: PropTypes.func.isRequired,
};

View File

@@ -0,0 +1,46 @@
import React from 'react';
import timezones from 'timezones-list';
import { useTranslation } from 'react-i18next';
import PropTypes from 'prop-types';
import { LOCAL_TIMEZONE_VALUE } from '../../../../helpers/constants';
export const Timezone = ({
timezone,
setTimezone,
}) => {
const [t] = useTranslation();
const onTimeZoneChange = (event) => {
setTimezone(event.target.value);
};
return (
<div className="schedule__timezone">
<label className="form__label form__label--with-desc mb-2">
{t('schedule_timezone')}
</label>
<select
className="form-control custom-select"
value={timezone}
onChange={onTimeZoneChange}
>
<option value={LOCAL_TIMEZONE_VALUE}>
{t('schedule_timezone')}
</option>
{/* TODO: get timezones from backend method when the method is ready */}
{timezones.map((zone) => (
<option key={zone.name} value={zone.tzCode}>
{zone.label}
</option>
))}
</select>
</div>
);
};
Timezone.propTypes = {
timezone: PropTypes.string.isRequired,
setTimezone: PropTypes.func.isRequired,
};

View File

@@ -0,0 +1,46 @@
export const getFullDayName = (t, abbreviation) => {
const dayMap = {
sun: t('sunday'),
mon: t('monday'),
tue: t('tuesday'),
wed: t('wednesday'),
thu: t('thursday'),
fri: t('friday'),
sat: t('saturday'),
};
return dayMap[abbreviation] || '';
};
export const getShortDayName = (t, abbreviation) => {
const dayMap = {
sun: t('sunday_short'),
mon: t('monday_short'),
tue: t('tuesday_short'),
wed: t('wednesday_short'),
thu: t('thursday_short'),
fri: t('friday_short'),
sat: t('saturday_short'),
};
return dayMap[abbreviation] || '';
};
export const getTimeFromMs = (value) => {
const selectedTime = new Date(value);
const hours = selectedTime.getUTCHours();
const minutes = selectedTime.getUTCMinutes();
return {
hours: hours.toString().padStart(2, '0'),
minutes: minutes.toString().padStart(2, '0'),
};
};
export const convertTimeToMs = (hours, minutes) => {
const selectedTime = new Date(0);
selectedTime.setUTCHours(parseInt(hours, 10));
selectedTime.setUTCMinutes(parseInt(minutes, 10));
return selectedTime.getTime();
};

View File

@@ -0,0 +1,140 @@
import React, { useState, useMemo } from 'react';
import { useTranslation } from 'react-i18next';
import PropTypes from 'prop-types';
import cn from 'classnames';
import { Modal } from './Modal';
import { getFullDayName, getShortDayName } from './helpers';
import { LOCAL_TIMEZONE_VALUE } from '../../../../helpers/constants';
import { TimePeriod } from './TimePeriod';
import './styles.css';
export const ScheduleForm = ({
schedule,
onScheduleSubmit,
clientForm,
}) => {
const [t] = useTranslation();
const [modalOpen, setModalOpen] = useState(false);
const [currentDay, setCurrentDay] = useState();
const onModalOpen = () => setModalOpen(true);
const onModalClose = () => setModalOpen(false);
const filteredScheduleKeys = useMemo(() => (
schedule ? Object.keys(schedule).filter((v) => v !== 'time_zone') : []
), [schedule]);
const scheduleMap = new Map();
filteredScheduleKeys.forEach((day) => scheduleMap.set(day, schedule[day]));
const onSubmit = (values) => {
onScheduleSubmit(values);
onModalClose();
};
const onDelete = (day) => {
scheduleMap.delete(day);
const scheduleWeek = Object.fromEntries(Array.from(scheduleMap.entries()));
onScheduleSubmit({
time_zone: schedule.time_zone,
...scheduleWeek,
});
};
const onEdit = (day) => {
setCurrentDay(day);
onModalOpen();
};
const onAdd = () => {
setCurrentDay(undefined);
onModalOpen();
};
return (
<div>
<div className="schedule__current-timezone">
{t('schedule_current_timezone', { value: schedule?.time_zone || LOCAL_TIMEZONE_VALUE })}
</div>
<div className="schedule__rows">
{filteredScheduleKeys.map((day) => {
const data = schedule[day];
if (!data) {
return undefined;
}
return (
<div key={day} className="schedule__row">
<div className="schedule__day">
{getFullDayName(t, day)}
</div>
<div className="schedule__day schedule__day--mobile">
{getShortDayName(t, day)}
</div>
<TimePeriod
startTimeMs={data.start}
endTimeMs={data.end}
/>
<div className="schedule__actions">
<button
type="button"
className="btn btn-icon btn-outline-primary btn-sm schedule__button"
title={t('edit_table_action')}
onClick={() => onEdit(day)}
>
<svg className="icons icon12">
<use xlinkHref="#edit" />
</svg>
</button>
<button
type="button"
className="btn btn-icon btn-outline-secondary btn-sm schedule__button"
title={t('delete_table_action')}
onClick={() => onDelete(day)}
>
<svg className="icons">
<use xlinkHref="#delete" />
</svg>
</button>
</div>
</div>
);
})}
</div>
<button
type="button"
className={cn(
'btn',
{ 'btn-outline-success btn-sm': clientForm },
{ 'btn-success btn-standard': !clientForm },
)}
onClick={onAdd}
>
{t('schedule_new')}
</button>
{modalOpen && (
<Modal
isOpen={modalOpen}
onClose={onModalClose}
onSubmit={onSubmit}
schedule={schedule}
currentDay={currentDay}
/>
)}
</div>
);
};
ScheduleForm.propTypes = {
schedule: PropTypes.object,
onScheduleSubmit: PropTypes.func.isRequired,
clientForm: PropTypes.bool,
};

View File

@@ -0,0 +1,134 @@
.schedule__row {
display: flex;
align-items: center;
justify-content: space-between;
margin-bottom: 8px;
}
.schedule__row:last-child {
margin-bottom: 0;
}
.schedule__rows {
margin-bottom: 24px;
}
.schedule__day {
display: none;
min-width: 110px;
}
.schedule__day--mobile {
display: block;
min-width: 50px;
}
@media screen and (min-width: 767px) {
.schedule__row {
justify-content: flex-start;
}
.schedule__day {
display: block;
}
.schedule__day--mobile {
display: none;
}
.schedule__actions {
margin-left: 32px;
white-space: nowrap;
}
}
.schedule__time {
min-width: 110px;
}
.schedule__button {
border: 0;
}
.schedule__days {
display: flex;
flex-wrap: wrap;
gap: 8px;
margin-bottom: 24px;
}
.schedule__button-day {
display: flex;
justify-content: center;
align-items: center;
min-width: 60px;
height: 32px;
font-size: 14px;
line-height: 14px;
color: #495057;
background-color: transparent;
border: 1px solid var(--card-border-color);
border-radius: 4px;
cursor: pointer;
outline: 0;
}
.schedule__button-day[data-active="true"] {
color: var(--btn-success-bgcolor);
border-color: var(--btn-success-bgcolor);
}
.schedule__time-wrap {
margin-bottom: 24px;
}
.schedule__time-row {
display: flex;
align-items: center;
gap: 16px;
}
.schedule__time-select {
display: flex;
align-items: center;
}
.schedule__error {
margin-top: 4px;
font-size: 13px;
color: #cd201f;
}
.schedule__timezone {
margin-bottom: 24px;
}
.schedule__current-timezone {
margin-bottom: 16px;
}
.schedule__info {
margin-bottom: 24px;
}
.schedule__notice {
font-size: 13px;
}
.schedule__info-title {
margin-bottom: 8px;
}
.schedule__info-row {
display: flex;
align-items: center;
margin-bottom: 4px;
}
.schedule__info-icon {
width: 24px;
height: 24px;
margin-right: 8px;
color: #495057;
flex-shrink: 0;
}

View File

@@ -4,9 +4,11 @@ import { useTranslation } from 'react-i18next';
import { useDispatch, useSelector } from 'react-redux';
import Form from './Form';
import Card from '../../ui/Card';
import { getBlockedServices, getAllBlockedServices, setBlockedServices } from '../../../actions/services';
import { getBlockedServices, getAllBlockedServices, updateBlockedServices } from '../../../actions/services';
import PageTitle from '../../ui/PageTitle';
import { ScheduleForm } from './ScheduleForm';
const getInitialDataForServices = (initial) => (initial ? initial.reduce(
(acc, service) => {
acc.blocked_services[service] = true;
@@ -33,10 +35,24 @@ const Services = () => {
.keys(values.blocked_services)
.filter((service) => values.blocked_services[service]);
dispatch(setBlockedServices(blocked_services));
dispatch(updateBlockedServices({
ids: blocked_services,
schedule: services.list.schedule,
}));
};
const initialValues = getInitialDataForServices(services.list);
const handleScheduleSubmit = (values) => {
dispatch(updateBlockedServices({
ids: services.list.ids,
schedule: values,
}));
};
const initialValues = getInitialDataForServices(services.list.ids);
if (!initialValues) {
return null;
}
return (
<>
@@ -57,6 +73,17 @@ const Services = () => {
/>
</div>
</Card>
<Card
title={t('schedule_services')}
subtitle={t('schedule_services_desc')}
bodyType="card-body box-body--settings"
>
<ScheduleForm
schedule={services.list.schedule}
onScheduleSubmit={handleScheduleSubmit}
/>
</Card>
</>
);
};

View File

@@ -6,7 +6,7 @@ import { Trans, useTranslation } from 'react-i18next';
import { useDispatch, useSelector } from 'react-redux';
import ReactTable from 'react-table';
import { getAllBlockedServices } from '../../../../actions/services';
import { getAllBlockedServices, getBlockedServices } from '../../../../actions/services';
import { initSettings } from '../../../../actions';
import {
splitByNewLine,
@@ -14,7 +14,7 @@ import {
sortIp,
getService,
} from '../../../../helpers/helpers';
import { MODAL_TYPE } from '../../../../helpers/constants';
import { MODAL_TYPE, LOCAL_TIMEZONE_VALUE } from '../../../../helpers/constants';
import Card from '../../../ui/Card';
import CellWrap from '../../../ui/CellWrap';
import LogsSearchLink from '../../../ui/LogsSearchLink';
@@ -45,6 +45,7 @@ const ClientsTable = ({
useEffect(() => {
dispatch(getAllBlockedServices());
dispatch(getBlockedServices());
dispatch(initSettings());
}, []);
@@ -112,6 +113,9 @@ const ClientsTable = ({
tags: [],
use_global_settings: true,
use_global_blocked_services: true,
blocked_services_schedule: {
time_zone: LOCAL_TIMEZONE_VALUE,
},
safe_search: { ...(safesearch || {}) },
};
};

View File

@@ -11,6 +11,7 @@ import Select from 'react-select';
import i18n from '../../../i18n';
import Tabs from '../../ui/Tabs';
import Examples from '../Dns/Upstream/Examples';
import { ScheduleForm } from '../../Filters/Services/ScheduleForm';
import { toggleAllServices, trimLinesAndRemoveEmpty, captitalizeWords } from '../../../helpers/helpers';
import {
renderInputField,
@@ -137,10 +138,10 @@ let Form = (props) => {
handleSubmit,
reset,
change,
pristine,
submitting,
useGlobalSettings,
useGlobalServices,
blockedServicesSchedule,
toggleClientModal,
processingAdding,
processingUpdating,
@@ -155,6 +156,10 @@ let Form = (props) => {
const [activeTabLabel, setActiveTabLabel] = useState('settings');
const handleScheduleSubmit = (values) => {
change('blocked_services_schedule', values);
};
const tabs = {
settings: {
title: 'settings',
@@ -269,6 +274,21 @@ let Form = (props) => {
</div>
</div>,
},
schedule_services: {
title: 'schedule_services',
component: (
<>
<div className="form__desc mb-4">
<Trans>schedule_services_desc_client</Trans>
</div>
<ScheduleForm
schedule={blockedServicesSchedule}
onScheduleSubmit={handleScheduleSubmit}
clientForm
/>
</>
),
},
upstream_dns: {
title: 'upstream_dns',
component: <div label="upstream" title={props.t('upstream_dns')}>
@@ -355,8 +375,12 @@ let Form = (props) => {
</div>
</div>
<Tabs controlClass="form" tabs={tabs} activeTabLabel={activeTabLabel}
setActiveTabLabel={setActiveTabLabel}>
<Tabs
controlClass="form"
tabs={tabs}
activeTabLabel={activeTabLabel}
setActiveTabLabel={setActiveTabLabel}
>
{activeTab}
</Tabs>
</div>
@@ -380,7 +404,6 @@ let Form = (props) => {
disabled={
submitting
|| invalid
|| pristine
|| processingAdding
|| processingUpdating
}
@@ -402,6 +425,7 @@ Form.propTypes = {
toggleClientModal: PropTypes.func.isRequired,
useGlobalSettings: PropTypes.bool,
useGlobalServices: PropTypes.bool,
blockedServicesSchedule: PropTypes.object,
t: PropTypes.func.isRequired,
processingAdding: PropTypes.bool.isRequired,
processingUpdating: PropTypes.bool.isRequired,
@@ -415,9 +439,11 @@ const selector = formValueSelector(FORM_NAME.CLIENT);
Form = connect((state) => {
const useGlobalSettings = selector(state, 'use_global_settings');
const useGlobalServices = selector(state, 'use_global_blocked_services');
const blockedServicesSchedule = selector(state, 'blocked_services_schedule');
return {
useGlobalSettings,
useGlobalServices,
blockedServicesSchedule,
};
})(Form);

View File

@@ -225,6 +225,20 @@ const Icons = () => (
<path stroke="currentColor" strokeWidth="1.5" strokeLinecap="round" d="M8.036 10.93l3.93 4.07 4.068-3.93" />
</g>
</symbol>
<symbol id="calendar" fill="none" height="24" viewBox="0 0 24 24" width="24" stroke="currentColor" strokeWidth="1.5" strokeLinecap="round" strokeLinejoin="round">
<rect x="4" y="5.5" width="16" height="14" rx="3" />
<path d="M12 4V7" />
<path d="M8 4L8 7" />
<path d="M16 4V7" />
<path d="M9.7397 15.5V11L8 13" />
<path d="M14.7397 15.5V11L13 13" />
</symbol>
<symbol id="watch" fill="none" height="24" viewBox="0 0 24 24" width="24" stroke="currentColor" strokeWidth="1.5" strokeLinecap="round" strokeLinejoin="round">
<circle cx="12" cy="12" r="9" />
<path d="M16.1215 12.1213H11.8789V7.87866" />
</symbol>
</svg>
);

View File

@@ -41,7 +41,8 @@
}
@media (min-width: 576px) {
.modal-dialog--clients {
.modal-dialog--clients,
.modal-dialog--schedule {
max-width: 650px;
}
}

View File

@@ -470,6 +470,10 @@ hr {
border-top: 1px solid rgba(0, 40, 100, 0.12);
}
[data-theme=dark] hr {
border-color: var(--card-border-color);
}
small,
.small {
font-size: 87.5%;

View File

@@ -1,6 +1,6 @@
import { connect } from 'react-redux';
import { initSettings, toggleSetting } from '../actions';
import { getBlockedServices, setBlockedServices } from '../actions/services';
import { getBlockedServices, updateBlockedServices } from '../actions/services';
import { getStatsConfig, setStatsConfig, resetStats } from '../actions/stats';
import { clearLogs, getLogsConfig, setLogsConfig } from '../actions/queryLogs';
import { getFilteringStatus, setFiltersConfig } from '../actions/filtering';
@@ -24,7 +24,7 @@ const mapDispatchToProps = {
initSettings,
toggleSetting,
getBlockedServices,
setBlockedServices,
updateBlockedServices,
getStatsConfig,
setStatsConfig,
resetStats,

View File

@@ -552,3 +552,5 @@ export const DISABLE_PROTECTION_TIMINGS = {
};
export const LOCAL_STORAGE_THEME_KEY = 'account_theme';
export const LOCAL_TIMEZONE_VALUE = 'Local';

View File

@@ -20,9 +20,9 @@ const services = handleActions(
processingAll: false,
}),
[actions.setBlockedServicesRequest]: (state) => ({ ...state, processingSet: true }),
[actions.setBlockedServicesFailure]: (state) => ({ ...state, processingSet: false }),
[actions.setBlockedServicesSuccess]: (state) => ({
[actions.updateBlockedServicesRequest]: (state) => ({ ...state, processingSet: true }),
[actions.updateBlockedServicesFailure]: (state) => ({ ...state, processingSet: false }),
[actions.updateBlockedServicesSuccess]: (state) => ({
...state,
processingSet: false,
}),
@@ -31,7 +31,7 @@ const services = handleActions(
processing: true,
processingAll: true,
processingSet: false,
list: [],
list: {},
allServices: [],
},
);