all: sync with master; upd chlog
This commit is contained in:
@@ -89,18 +89,18 @@ body {
|
||||
}
|
||||
|
||||
.container--wrap {
|
||||
min-height: calc(100vh - 160px);
|
||||
min-height: calc(100vh - 372px);
|
||||
}
|
||||
|
||||
@media screen and (min-width: 768px) {
|
||||
.container--wrap {
|
||||
min-height: calc(100vh - 168px);
|
||||
}
|
||||
}
|
||||
|
||||
@media screen and (min-width: 992px) {
|
||||
.container--wrap {
|
||||
min-height: calc(100vh - 117px);
|
||||
}
|
||||
}
|
||||
|
||||
@media screen and (max-width: 992px) {
|
||||
.container--wrap {
|
||||
min-height: calc(100vh);
|
||||
min-height: calc(100vh - 187px);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -192,7 +192,7 @@ const App = () => {
|
||||
<LoadingBar className="loading-bar" updateTime={1000} />
|
||||
<Header />
|
||||
<ProtectionTimer />
|
||||
<div className="container container--wrap pb-5">
|
||||
<div className="container container--wrap pb-5 pt-5">
|
||||
{processing && <Loading />}
|
||||
{!isCoreRunning && <div className="row row-cards">
|
||||
<div className="col-lg-12">
|
||||
|
||||
79
client/src/components/Dashboard/UpstreamAvgTime.js
Normal file
79
client/src/components/Dashboard/UpstreamAvgTime.js
Normal file
@@ -0,0 +1,79 @@
|
||||
import React from 'react';
|
||||
import ReactTable from 'react-table';
|
||||
import PropTypes from 'prop-types';
|
||||
import round from 'lodash/round';
|
||||
import { withTranslation, Trans } from 'react-i18next';
|
||||
|
||||
import Card from '../ui/Card';
|
||||
import DomainCell from './DomainCell';
|
||||
|
||||
const TimeCell = ({ value }) => {
|
||||
if (!value) {
|
||||
return '–';
|
||||
}
|
||||
|
||||
const valueInMilliseconds = round(value * 1000);
|
||||
|
||||
return (
|
||||
<div className="logs__row o-hidden">
|
||||
<span className="logs__text logs__text--full" title={valueInMilliseconds}>
|
||||
{valueInMilliseconds} ms
|
||||
</span>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
TimeCell.propTypes = {
|
||||
value: PropTypes.oneOfType([
|
||||
PropTypes.string,
|
||||
PropTypes.number,
|
||||
]),
|
||||
};
|
||||
|
||||
const UpstreamAvgTime = ({
|
||||
t,
|
||||
refreshButton,
|
||||
topUpstreamsAvgTime,
|
||||
subtitle,
|
||||
}) => (
|
||||
<Card
|
||||
title={t('average_processing_time')}
|
||||
subtitle={subtitle}
|
||||
bodyType="card-table"
|
||||
refresh={refreshButton}
|
||||
>
|
||||
<ReactTable
|
||||
data={topUpstreamsAvgTime.map(({ name: domain, count }) => ({
|
||||
domain,
|
||||
count,
|
||||
}))}
|
||||
columns={[
|
||||
{
|
||||
Header: <Trans>upstream</Trans>,
|
||||
accessor: 'domain',
|
||||
Cell: DomainCell,
|
||||
},
|
||||
{
|
||||
Header: <Trans>processing_time</Trans>,
|
||||
accessor: 'count',
|
||||
maxWidth: 190,
|
||||
Cell: TimeCell,
|
||||
},
|
||||
]}
|
||||
showPagination={false}
|
||||
noDataText={t('no_upstreams_data_found')}
|
||||
minRows={6}
|
||||
defaultPageSize={100}
|
||||
className="-highlight card-table-overflow--limited stats__table"
|
||||
/>
|
||||
</Card>
|
||||
);
|
||||
|
||||
UpstreamAvgTime.propTypes = {
|
||||
topUpstreamsAvgTime: PropTypes.array.isRequired,
|
||||
refreshButton: PropTypes.node.isRequired,
|
||||
subtitle: PropTypes.string.isRequired,
|
||||
t: PropTypes.func.isRequired,
|
||||
};
|
||||
|
||||
export default withTranslation()(UpstreamAvgTime);
|
||||
81
client/src/components/Dashboard/UpstreamResponses.js
Normal file
81
client/src/components/Dashboard/UpstreamResponses.js
Normal file
@@ -0,0 +1,81 @@
|
||||
import React from 'react';
|
||||
import ReactTable from 'react-table';
|
||||
import PropTypes from 'prop-types';
|
||||
import { withTranslation, Trans } from 'react-i18next';
|
||||
|
||||
import Card from '../ui/Card';
|
||||
import Cell from '../ui/Cell';
|
||||
import DomainCell from './DomainCell';
|
||||
|
||||
import { getPercent } from '../../helpers/helpers';
|
||||
import { STATUS_COLORS } from '../../helpers/constants';
|
||||
|
||||
const CountCell = (totalBlocked) => (
|
||||
function cell(row) {
|
||||
const { value } = row;
|
||||
const percent = getPercent(totalBlocked, value);
|
||||
|
||||
return (
|
||||
<Cell
|
||||
value={value}
|
||||
percent={percent}
|
||||
color={STATUS_COLORS.green}
|
||||
/>
|
||||
);
|
||||
}
|
||||
);
|
||||
|
||||
const getTotalUpstreamRequests = (stats) => {
|
||||
let total = 0;
|
||||
stats.forEach(({ count }) => { total += count; });
|
||||
|
||||
return total;
|
||||
};
|
||||
|
||||
const UpstreamResponses = ({
|
||||
t,
|
||||
refreshButton,
|
||||
topUpstreamsResponses,
|
||||
subtitle,
|
||||
}) => (
|
||||
<Card
|
||||
title={t('top_upstreams')}
|
||||
subtitle={subtitle}
|
||||
bodyType="card-table"
|
||||
refresh={refreshButton}
|
||||
>
|
||||
<ReactTable
|
||||
data={topUpstreamsResponses.map(({ name: domain, count }) => ({
|
||||
domain,
|
||||
count,
|
||||
}))}
|
||||
columns={[
|
||||
{
|
||||
Header: <Trans>upstream</Trans>,
|
||||
accessor: 'domain',
|
||||
Cell: DomainCell,
|
||||
},
|
||||
{
|
||||
Header: <Trans>requests_count</Trans>,
|
||||
accessor: 'count',
|
||||
maxWidth: 190,
|
||||
Cell: CountCell(getTotalUpstreamRequests(topUpstreamsResponses)),
|
||||
},
|
||||
]}
|
||||
showPagination={false}
|
||||
noDataText={t('no_upstreams_data_found')}
|
||||
minRows={6}
|
||||
defaultPageSize={100}
|
||||
className="-highlight card-table-overflow--limited stats__table"
|
||||
/>
|
||||
</Card>
|
||||
);
|
||||
|
||||
UpstreamResponses.propTypes = {
|
||||
topUpstreamsResponses: PropTypes.array.isRequired,
|
||||
refreshButton: PropTypes.node.isRequired,
|
||||
subtitle: PropTypes.string.isRequired,
|
||||
t: PropTypes.func.isRequired,
|
||||
};
|
||||
|
||||
export default withTranslation()(UpstreamResponses);
|
||||
@@ -21,6 +21,8 @@ import PageTitle from '../ui/PageTitle';
|
||||
import Loading from '../ui/Loading';
|
||||
import './Dashboard.css';
|
||||
import Dropdown from '../ui/Dropdown';
|
||||
import UpstreamResponses from './UpstreamResponses';
|
||||
import UpstreamAvgTime from './UpstreamAvgTime';
|
||||
|
||||
const Dashboard = ({
|
||||
getAccessList,
|
||||
@@ -136,12 +138,12 @@ const Dashboard = ({
|
||||
<PageTitle title={t('dashboard')} containerClass="page-title--dashboard">
|
||||
<div className="page-title__protection">
|
||||
<button
|
||||
type="button"
|
||||
className={buttonClass}
|
||||
onClick={() => {
|
||||
toggleProtection(protectionEnabled);
|
||||
}}
|
||||
disabled={processingProtection}
|
||||
type="button"
|
||||
className={buttonClass}
|
||||
onClick={() => {
|
||||
toggleProtection(protectionEnabled);
|
||||
}}
|
||||
disabled={processingProtection}
|
||||
>
|
||||
{protectionDisabledDuration
|
||||
? `${t('enable_protection_timer')} ${getRemaningTimeText(protectionDisabledDuration)}`
|
||||
@@ -160,9 +162,9 @@ const Dashboard = ({
|
||||
</Dropdown>}
|
||||
</div>
|
||||
<button
|
||||
type="button"
|
||||
className="btn btn-outline-primary btn-sm"
|
||||
onClick={getAllStats}
|
||||
type="button"
|
||||
className="btn btn-outline-primary btn-sm"
|
||||
onClick={getAllStats}
|
||||
>
|
||||
<Trans>refresh_statics</Trans>
|
||||
</button>
|
||||
@@ -185,53 +187,67 @@ const Dashboard = ({
|
||||
</div>
|
||||
)}
|
||||
<Statistics
|
||||
interval={msToDays(stats.interval)}
|
||||
dnsQueries={stats.dnsQueries}
|
||||
blockedFiltering={stats.blockedFiltering}
|
||||
replacedSafebrowsing={stats.replacedSafebrowsing}
|
||||
replacedParental={stats.replacedParental}
|
||||
numDnsQueries={stats.numDnsQueries}
|
||||
numBlockedFiltering={stats.numBlockedFiltering}
|
||||
numReplacedSafebrowsing={stats.numReplacedSafebrowsing}
|
||||
numReplacedParental={stats.numReplacedParental}
|
||||
refreshButton={refreshButton}
|
||||
interval={msToDays(stats.interval)}
|
||||
dnsQueries={stats.dnsQueries}
|
||||
blockedFiltering={stats.blockedFiltering}
|
||||
replacedSafebrowsing={stats.replacedSafebrowsing}
|
||||
replacedParental={stats.replacedParental}
|
||||
numDnsQueries={stats.numDnsQueries}
|
||||
numBlockedFiltering={stats.numBlockedFiltering}
|
||||
numReplacedSafebrowsing={stats.numReplacedSafebrowsing}
|
||||
numReplacedParental={stats.numReplacedParental}
|
||||
refreshButton={refreshButton}
|
||||
/>
|
||||
</div>
|
||||
<div className="col-lg-6">
|
||||
<Counters
|
||||
subtitle={subtitle}
|
||||
refreshButton={refreshButton}
|
||||
subtitle={subtitle}
|
||||
refreshButton={refreshButton}
|
||||
/>
|
||||
</div>
|
||||
<div className="col-lg-6">
|
||||
<Clients
|
||||
subtitle={subtitle}
|
||||
dnsQueries={stats.numDnsQueries}
|
||||
topClients={stats.topClients}
|
||||
clients={dashboard.clients}
|
||||
autoClients={dashboard.autoClients}
|
||||
refreshButton={refreshButton}
|
||||
processingAccessSet={access.processingSet}
|
||||
disallowedClients={access.disallowed_clients}
|
||||
subtitle={subtitle}
|
||||
dnsQueries={stats.numDnsQueries}
|
||||
topClients={stats.topClients}
|
||||
clients={dashboard.clients}
|
||||
autoClients={dashboard.autoClients}
|
||||
refreshButton={refreshButton}
|
||||
processingAccessSet={access.processingSet}
|
||||
disallowedClients={access.disallowed_clients}
|
||||
/>
|
||||
</div>
|
||||
<div className="col-lg-6">
|
||||
<QueriedDomains
|
||||
subtitle={subtitle}
|
||||
dnsQueries={stats.numDnsQueries}
|
||||
topQueriedDomains={stats.topQueriedDomains}
|
||||
refreshButton={refreshButton}
|
||||
subtitle={subtitle}
|
||||
dnsQueries={stats.numDnsQueries}
|
||||
topQueriedDomains={stats.topQueriedDomains}
|
||||
refreshButton={refreshButton}
|
||||
/>
|
||||
</div>
|
||||
<div className="col-lg-6">
|
||||
<BlockedDomains
|
||||
subtitle={subtitle}
|
||||
topBlockedDomains={stats.topBlockedDomains}
|
||||
blockedFiltering={stats.numBlockedFiltering}
|
||||
replacedSafebrowsing={stats.numReplacedSafebrowsing}
|
||||
replacedSafesearch={stats.numReplacedSafesearch}
|
||||
replacedParental={stats.numReplacedParental}
|
||||
refreshButton={refreshButton}
|
||||
subtitle={subtitle}
|
||||
topBlockedDomains={stats.topBlockedDomains}
|
||||
blockedFiltering={stats.numBlockedFiltering}
|
||||
replacedSafebrowsing={stats.numReplacedSafebrowsing}
|
||||
replacedSafesearch={stats.numReplacedSafesearch}
|
||||
replacedParental={stats.numReplacedParental}
|
||||
refreshButton={refreshButton}
|
||||
/>
|
||||
</div>
|
||||
<div className="col-lg-6">
|
||||
<UpstreamResponses
|
||||
subtitle={subtitle}
|
||||
topUpstreamsResponses={stats.topUpstreamsResponses}
|
||||
refreshButton={refreshButton}
|
||||
/>
|
||||
</div>
|
||||
<div className="col-lg-6">
|
||||
<UpstreamAvgTime
|
||||
subtitle={subtitle}
|
||||
topUpstreamsAvgTime={stats.topUpstreamsAvgTime}
|
||||
refreshButton={refreshButton}
|
||||
/>
|
||||
</div>
|
||||
</div>}
|
||||
|
||||
@@ -134,7 +134,6 @@ const Form = (props) => {
|
||||
component={renderInputField}
|
||||
className="form-control"
|
||||
placeholder={t('enter_name_hint')}
|
||||
validate={[validateRequiredValue]}
|
||||
normalizeOnBlur={(data) => data.trim()}
|
||||
/>
|
||||
</div>
|
||||
|
||||
220
client/src/components/Filters/Services/ScheduleForm/Modal.js
Normal file
220
client/src/components/Filters/Services/ScheduleForm/Modal.js
Normal 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,
|
||||
};
|
||||
@@ -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>
|
||||
–
|
||||
<time>{endTime.hours}:{endTime.minutes}</time>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
TimePeriod.propTypes = {
|
||||
startTimeMs: PropTypes.number.isRequired,
|
||||
endTimeMs: PropTypes.number.isRequired,
|
||||
};
|
||||
@@ -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>
|
||||
:
|
||||
<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,
|
||||
};
|
||||
@@ -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,
|
||||
};
|
||||
@@ -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();
|
||||
};
|
||||
140
client/src/components/Filters/Services/ScheduleForm/index.js
Normal file
140
client/src/components/Filters/Services/ScheduleForm/index.js
Normal 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,
|
||||
};
|
||||
134
client/src/components/Filters/Services/ScheduleForm/styles.css
Normal file
134
client/src/components/Filters/Services/ScheduleForm/styles.css
Normal 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;
|
||||
}
|
||||
@@ -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>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -74,6 +74,12 @@
|
||||
color: #295a9f;
|
||||
}
|
||||
|
||||
[data-theme="dark"] .logs__text--link,
|
||||
[data-theme="dark"] .logs__text--link:hover,
|
||||
[data-theme="dark"] .logs__text--link:focus {
|
||||
color: var(--gray-f3);
|
||||
}
|
||||
|
||||
.icon--selected {
|
||||
background-color: var(--gray-f3);
|
||||
border: solid 1px var(--gray-d8);
|
||||
|
||||
@@ -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 || {}) },
|
||||
};
|
||||
};
|
||||
|
||||
@@ -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);
|
||||
|
||||
|
||||
@@ -179,6 +179,30 @@ const Form = ({
|
||||
<Examples />
|
||||
<hr />
|
||||
</div>
|
||||
<div className="col-12">
|
||||
<label
|
||||
className="form__label form__label--with-desc"
|
||||
htmlFor="fallback_dns"
|
||||
>
|
||||
<Trans>fallback_dns_title</Trans>
|
||||
</label>
|
||||
<div className="form__desc form__desc--top">
|
||||
<Trans>fallback_dns_desc</Trans>
|
||||
</div>
|
||||
<Field
|
||||
id="fallback_dns"
|
||||
name="fallback_dns"
|
||||
component={renderTextareaField}
|
||||
type="text"
|
||||
className="form-control form-control--textarea form-control--textarea-small font-monospace"
|
||||
placeholder={t('fallback_dns_placeholder')}
|
||||
disabled={processingSetConfig}
|
||||
normalizeOnBlur={removeEmptyLines}
|
||||
/>
|
||||
</div>
|
||||
<div className="col-12">
|
||||
<hr />
|
||||
</div>
|
||||
<div className="col-12 mb-2">
|
||||
<label
|
||||
className="form__label form__label--with-desc"
|
||||
@@ -286,6 +310,7 @@ Form.propTypes = {
|
||||
invalid: PropTypes.bool,
|
||||
initialValues: PropTypes.object,
|
||||
upstream_dns: PropTypes.string,
|
||||
fallback_dns: PropTypes.string,
|
||||
bootstrap_dns: PropTypes.string,
|
||||
};
|
||||
|
||||
|
||||
@@ -10,6 +10,7 @@ const Upstream = () => {
|
||||
const dispatch = useDispatch();
|
||||
const {
|
||||
upstream_dns,
|
||||
fallback_dns,
|
||||
bootstrap_dns,
|
||||
upstream_mode,
|
||||
resolve_clients,
|
||||
@@ -21,6 +22,7 @@ const Upstream = () => {
|
||||
|
||||
const handleSubmit = (values) => {
|
||||
const {
|
||||
fallback_dns,
|
||||
bootstrap_dns,
|
||||
upstream_dns,
|
||||
upstream_mode,
|
||||
@@ -30,6 +32,7 @@ const Upstream = () => {
|
||||
} = values;
|
||||
|
||||
const dnsConfig = {
|
||||
fallback_dns,
|
||||
bootstrap_dns,
|
||||
upstream_mode,
|
||||
resolve_clients,
|
||||
@@ -52,6 +55,7 @@ const Upstream = () => {
|
||||
<Form
|
||||
initialValues={{
|
||||
upstream_dns: upstreamDns,
|
||||
fallback_dns,
|
||||
bootstrap_dns,
|
||||
upstream_mode,
|
||||
resolve_clients,
|
||||
|
||||
@@ -13,8 +13,11 @@ import flow from 'lodash/flow';
|
||||
import {
|
||||
CheckboxField,
|
||||
toFloatNumber,
|
||||
renderTextareaField, renderInputField, renderRadioField,
|
||||
renderTextareaField,
|
||||
renderInputField,
|
||||
renderRadioField,
|
||||
} from '../../../helpers/form';
|
||||
import { trimLinesAndRemoveEmpty } from '../../../helpers/helpers';
|
||||
import {
|
||||
FORM_NAME,
|
||||
QUERY_LOG_INTERVALS_DAYS,
|
||||
@@ -147,6 +150,7 @@ let Form = (props) => {
|
||||
component={renderTextareaField}
|
||||
placeholder={t('ignore_domains')}
|
||||
disabled={processing}
|
||||
normalizeOnBlur={trimLinesAndRemoveEmpty}
|
||||
/>
|
||||
</div>
|
||||
<div className="mt-5">
|
||||
|
||||
@@ -24,6 +24,7 @@ import {
|
||||
CUSTOM_INTERVAL,
|
||||
RETENTION_RANGE,
|
||||
} from '../../../helpers/constants';
|
||||
import { trimLinesAndRemoveEmpty } from '../../../helpers/helpers';
|
||||
import '../FormButton.css';
|
||||
|
||||
const getIntervalTitle = (intervalMs, t) => {
|
||||
@@ -135,6 +136,7 @@ let Form = (props) => {
|
||||
component={renderTextareaField}
|
||||
placeholder={t('ignore_domains')}
|
||||
disabled={processing}
|
||||
normalizeOnBlur={trimLinesAndRemoveEmpty}
|
||||
/>
|
||||
</div>
|
||||
<div className="mt-5">
|
||||
|
||||
@@ -1,25 +1,39 @@
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
|
||||
import LogsSearchLink from './LogsSearchLink';
|
||||
import { formatNumber } from '../../helpers/helpers';
|
||||
|
||||
const Cell = ({
|
||||
value, percent, color, search,
|
||||
}) => <div className="stats__row">
|
||||
<div className="stats__row-value mb-1">
|
||||
<strong><LogsSearchLink search={search}>{formatNumber(value)}</LogsSearchLink></strong>
|
||||
<small className="ml-3 text-muted">{percent}%</small>
|
||||
value,
|
||||
percent,
|
||||
color,
|
||||
search,
|
||||
}) => (
|
||||
<div className="stats__row">
|
||||
<div className="stats__row-value mb-1">
|
||||
<strong>
|
||||
{search ? (
|
||||
<LogsSearchLink search={search}>
|
||||
{formatNumber(value)}
|
||||
</LogsSearchLink>
|
||||
) : (
|
||||
formatNumber(value)
|
||||
)}
|
||||
</strong>
|
||||
<small className="ml-3 text-muted">{percent}%</small>
|
||||
</div>
|
||||
<div className="progress progress-xs">
|
||||
<div
|
||||
className="progress-bar"
|
||||
style={{
|
||||
width: `${percent}%`,
|
||||
backgroundColor: color,
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div className="progress progress-xs">
|
||||
<div
|
||||
className="progress-bar"
|
||||
style={{
|
||||
width: `${percent}%`,
|
||||
backgroundColor: color,
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
</div>;
|
||||
);
|
||||
|
||||
Cell.propTypes = {
|
||||
value: PropTypes.number.isRequired,
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
|
||||
|
||||
@@ -44,7 +44,7 @@ const Line = ({
|
||||
enableGridY={false}
|
||||
enablePoints={false}
|
||||
xFormat={(x) => {
|
||||
if (interval === 1 || interval === 7) {
|
||||
if (interval >= 0 && interval <= 7) {
|
||||
const hoursAgo = subHours(Date.now(), 24 * interval);
|
||||
return dateFormat(addHours(hoursAgo, x), 'D MMM HH:00');
|
||||
}
|
||||
|
||||
@@ -41,7 +41,8 @@
|
||||
}
|
||||
|
||||
@media (min-width: 576px) {
|
||||
.modal-dialog--clients {
|
||||
.modal-dialog--clients,
|
||||
.modal-dialog--schedule {
|
||||
max-width: 650px;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -6,7 +6,7 @@
|
||||
.page-header--logs {
|
||||
flex-direction: row;
|
||||
align-items: flex-end;
|
||||
margin: 2rem 0 2.8rem;
|
||||
margin: 0.5rem 0 2.8rem;
|
||||
}
|
||||
|
||||
.page-header--logs .page-title {
|
||||
@@ -18,7 +18,7 @@
|
||||
.page-header--logs {
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
margin: 1.1rem 0;
|
||||
margin-bottom: 0 0 1.1rem;
|
||||
}
|
||||
|
||||
.page-header--logs .page-title {
|
||||
|
||||
@@ -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%;
|
||||
@@ -10204,7 +10208,7 @@ body.fixed-header .page {
|
||||
-ms-flex-align: center;
|
||||
align-items: center;
|
||||
flex-direction: column;
|
||||
margin: 1.5rem 0 1.5rem;
|
||||
margin: 0 0 1.5rem;
|
||||
-ms-flex-wrap: wrap;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user