-
+
);
};
diff --git a/client_v2/src/components/Toasts/index.tsx b/client_v2/src/components/Toasts/index.tsx
index 4664b516..b26efc36 100644
--- a/client_v2/src/components/Toasts/index.tsx
+++ b/client_v2/src/components/Toasts/index.tsx
@@ -5,7 +5,7 @@ import { CSSTransition, TransitionGroup } from 'react-transition-group';
import { TOAST_TRANSITION_TIMEOUT } from '../../helpers/constants';
import Toast from './Toast';
-import './Toast.css';
+import './Toast.pcss';
import { RootState } from '../../initialState';
const Toasts = () => {
diff --git a/client_v2/src/components/ui/Card.css b/client_v2/src/components/ui/Card.css
new file mode 100644
index 00000000..5323c9a8
--- /dev/null
+++ b/client_v2/src/components/ui/Card.css
@@ -0,0 +1,155 @@
+.card-header {
+ align-items: center;
+ justify-content: space-between;
+ padding: 0.6rem 1.5rem;
+ flex-shrink: 0;
+}
+
+.card-subtitle {
+ margin: 4px 0;
+ line-height: 1.4;
+}
+
+.card-table-overflow {
+ overflow-y: auto;
+ max-height: 100%;
+}
+
+.card-table-overflow--limited {
+ overflow-y: auto;
+ max-height: 17.5rem;
+}
+
+.dashboard .card-table {
+ overflow: hidden;
+}
+
+.dashboard .card-table-overflow--limited {
+ max-height: 292px;
+}
+
+.dashboard .ReactTable .rt-tr-group {
+ min-height: 52px;
+}
+
+.card-actions {
+ margin-top: 20px;
+}
+
+.card-actions-top {
+ margin-bottom: 20px;
+}
+
+.card-graph {
+ display: flex;
+ align-items: center;
+ justify-content: center;
+}
+
+.card-body--status {
+ padding: 2.5rem 1.5rem;
+ text-align: center;
+}
+
+.card-body--loading {
+ position: relative;
+}
+
+.card-body--loading:before {
+ content: '';
+ position: absolute;
+ top: 0;
+ left: 0;
+ z-index: 100;
+ width: 100%;
+ height: 100%;
+ background-color: rgba(255, 255, 255, 0.6);
+}
+
+.card-body--loading:after {
+ content: '';
+ position: absolute;
+ z-index: 101;
+ left: 50%;
+ top: 50%;
+ width: 40px;
+ height: 40px;
+ margin-top: -20px;
+ margin-left: -20px;
+ background-image: url('data:image/svg+xml;charset=utf-8,%3Csvg%20xmlns%3D%22http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%22%20viewBox%3D%220%200%2047.6%2047.6%22%20height%3D%22100%25%22%20width%3D%22100%25%22%3E%3Cpath%20opacity%3D%22.235%22%20fill%3D%22%23979797%22%20d%3D%22M44.4%2011.9l-5.2%203c1.5%202.6%202.4%205.6%202.4%208.9%200%209.8-8%2017.8-17.8%2017.8-6.6%200-12.3-3.6-15.4-8.9l-5.2%203C7.3%2042.8%2015%2047.6%2023.8%2047.6c13.1%200%2023.8-10.7%2023.8-23.8%200-4.3-1.2-8.4-3.2-11.9z%22%2F%3E%3Cpath%20fill%3D%22%2366b574%22%20d%3D%22M3.2%2035.7C0%2030.2-.8%2023.8.8%2017.6%202.5%2011.5%206.4%206.4%2011.9%203.2%2017.4%200%2023.8-.8%2030%20.8c6.1%201.6%2011.3%205.6%2014.4%2011.1l-5.2%203c-2.4-4.1-6.2-7.1-10.8-8.3C23.8%205.4%2019%206%2014.9%208.4s-7.1%206.2-8.3%2010.8c-1.2%204.6-.6%209.4%201.8%2013.5l-5.2%203z%22%2F%3E%3C%2Fsvg%3E');
+ will-change: transform;
+ animation: clockwise 2s linear infinite;
+}
+
+.card-title-stats {
+ font-size: 13px;
+ color: #9aa0ac;
+}
+
+.card-body-stats {
+ position: relative;
+ flex: 1 1 auto;
+ margin: 0;
+ padding: 1rem 1.5rem;
+}
+
+.card-value-stats {
+ display: block;
+ font-size: 2.1rem;
+ line-height: 2.7rem;
+ height: 2.7rem;
+ text-overflow: ellipsis;
+ white-space: nowrap;
+ overflow: hidden;
+}
+
+.card-value-percent {
+ position: absolute;
+ top: 15px;
+ right: 15px;
+ font-size: 0.9rem;
+ line-height: 1;
+ height: auto;
+}
+
+.card-value-percent:after {
+ content: '%';
+}
+
+.card--full {
+ height: calc(100% - 1.5rem);
+}
+
+.card-wrap {
+ display: flex;
+ flex-direction: column;
+ height: 100%;
+}
+
+@media screen and (min-width: 1280px) {
+ .card-title-stats {
+ font-size: 14px;
+ }
+}
+
+@media (min-width: 992px) {
+ .dashboard .card:not(.card--full) {
+ height: 360px;
+ }
+}
+
+.card .logs__cell--red {
+ background-color: #fff4f2;
+}
+
+.card .logs__cell--green {
+ background-color: #f1faf3;
+}
+
+.card .logs__row--blue {
+ background-color: #ecf7ff;
+}
+
+[data-theme='dark'] .card .logs__row--blue {
+ background-color: var(--logs__row--blue-bgcolor);
+}
diff --git a/client_v2/src/components/ui/Card.tsx b/client_v2/src/components/ui/Card.tsx
new file mode 100644
index 00000000..27fcf9a1
--- /dev/null
+++ b/client_v2/src/components/ui/Card.tsx
@@ -0,0 +1,33 @@
+import React from 'react';
+
+import './Card.css';
+
+interface CardProps {
+ id?: string;
+ title?: string;
+ subtitle?: string;
+ bodyType?: string;
+ type?: string;
+ refresh?: React.ReactNode;
+ children: React.ReactNode;
+}
+
+const Card = ({ type, id, title, subtitle, refresh, bodyType, children }: CardProps) => (
+
+ {(title || subtitle) && (
+
+
+ {title &&
{title}
}
+
+ {subtitle &&
}
+
+
+ {refresh &&
{refresh}
}
+
+ )}
+
+
{children}
+
+);
+
+export default Card;
diff --git a/client_v2/src/components/ui/Controls/Checkbox/checkbox.css b/client_v2/src/components/ui/Controls/Checkbox/checkbox.css
new file mode 100644
index 00000000..40e35a08
--- /dev/null
+++ b/client_v2/src/components/ui/Controls/Checkbox/checkbox.css
@@ -0,0 +1,113 @@
+.checkbox {
+ display: inline-block;
+ margin: 0;
+}
+
+.checkbox--single {
+ display: block;
+ margin: 2px auto 6px;
+}
+
+.checkbox--single .checkbox__label:before {
+ margin-right: 0;
+}
+
+.checkbox--settings .checkbox__label:before {
+ top: 2px;
+ margin-right: 20px;
+}
+
+.checkbox--settings .checkbox__label-title {
+ margin-bottom: 2px;
+ font-weight: 600;
+}
+
+.checkbox--form .checkbox__label:before {
+ top: 1px;
+ margin-right: 10px;
+}
+
+.checkbox__label {
+ position: relative;
+ display: flex;
+ align-items: flex-start;
+ justify-content: center;
+ font-size: 14px;
+ font-weight: 400;
+ user-select: none;
+ cursor: pointer;
+}
+
+.checkbox__label:before {
+ content: '';
+ position: relative;
+ top: 1px;
+ display: inline-block;
+ vertical-align: middle;
+ width: 20px;
+ height: 20px;
+ min-width: 20px;
+ margin-right: 10px;
+ background-color: var(--checkbox-bg);
+ background-repeat: no-repeat;
+ background-position: center center;
+ background-size: 12px 10px;
+ border-radius: 3px;
+ transition:
+ 0.3s ease-in-out box-shadow,
+ 0.3s ease-in-out opacity;
+}
+
+.checkbox__label .checkbox__label-text {
+ line-height: 1.3;
+}
+
+.checkbox__label .checkbox__label-text .md__paragraph {
+ display: inline-block;
+ vertical-align: baseline;
+ margin: 0;
+ text-align: left;
+ line-height: 1.3;
+}
+
+.checkbox__input {
+ position: absolute;
+ opacity: 0;
+}
+
+.checkbox__input:checked + .checkbox__label:before {
+ background-image: url(data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCAxMi4zIDkuMiIgZmlsbD0ibm9uZSIgc3Ryb2tlPSIjMDAwIiBzdHJva2UtbGluZWNhcD0icm91bmQiPjxwYXRoIGQ9Ik0xMS44IDAuNUw1LjMgOC41IDAuNSA0LjIiLz48L3N2Zz4=);
+}
+
+.checkbox__input:focus + .checkbox__label:before {
+ box-shadow: 0 0 1px 1px rgba(74, 74, 74, 0.32);
+}
+
+.checkbox__input:disabled + .checkbox__label {
+ cursor: default;
+ color: var(--gray);
+}
+
+.checkbox__input:disabled + .checkbox__label:before {
+ opacity: 0.6;
+}
+
+.checkbox__label-text {
+ max-width: 515px;
+ line-height: 1.5;
+}
+
+.checkbox__label-text--long {
+ max-width: initial;
+}
+
+.checkbox__label-title {
+ display: block;
+ line-height: 1.5;
+}
+
+.checkbox__label-subtitle {
+ display: block;
+ line-height: 1.5;
+ color: var(--scolor);
+}
diff --git a/client_v2/src/components/ui/Controls/Checkbox/index.tsx b/client_v2/src/components/ui/Controls/Checkbox/index.tsx
new file mode 100644
index 00000000..09ed43fa
--- /dev/null
+++ b/client_v2/src/components/ui/Controls/Checkbox/index.tsx
@@ -0,0 +1,50 @@
+import React, { forwardRef, ReactNode } from 'react';
+import clsx from 'clsx';
+
+import './checkbox.css';
+
+type Props = {
+ title: string;
+ subtitle?: ReactNode;
+ value: boolean;
+ name?: string;
+ disabled?: boolean;
+ className?: string;
+ error?: string;
+ onChange: (value: boolean) => void;
+ onBlur?: () => void;
+};
+
+export const Checkbox = forwardRef
(
+ (
+ { title, subtitle, value, name, disabled, error, className = 'checkbox--form', onChange, onBlur, ...rest },
+ ref,
+ ) => (
+ <>
+
+ {error && {error}
}
+ >
+ ),
+);
+
+Checkbox.displayName = 'Checkbox';
diff --git a/client_v2/src/components/ui/Controls/Textarea.tsx b/client_v2/src/components/ui/Controls/Textarea.tsx
new file mode 100644
index 00000000..4c9dfe04
--- /dev/null
+++ b/client_v2/src/components/ui/Controls/Textarea.tsx
@@ -0,0 +1,45 @@
+import React, { ComponentProps, forwardRef } from 'react';
+import clsx from 'clsx';
+import { trimLinesAndRemoveEmpty } from '../../../helpers/helpers';
+
+type Props = ComponentProps<'textarea'> & {
+ className?: string;
+ wrapperClassName?: string;
+ label?: string;
+ desc?: string;
+ error?: string;
+ trimOnBlur?: boolean;
+};
+
+export const Textarea = forwardRef(
+ ({ name, label, desc, className, wrapperClassName, error, trimOnBlur, onBlur, ...rest }, ref) => (
+
+ {label && (
+
+ )}
+ {desc &&
{desc}
}
+
+ ),
+);
+
+Textarea.displayName = 'Textarea';
diff --git a/client_v2/src/components/ui/Loading.css b/client_v2/src/components/ui/Loading.css
new file mode 100644
index 00000000..7a3d71e1
--- /dev/null
+++ b/client_v2/src/components/ui/Loading.css
@@ -0,0 +1,52 @@
+.loading {
+ position: relative;
+ z-index: 101;
+ opacity: 0;
+ animation: opacity 0.2s linear 0.2s forwards;
+}
+
+.loading:before {
+ content: '';
+ position: fixed;
+ top: 0;
+ left: 0;
+ z-index: 100;
+ width: 100%;
+ min-height: 100vh;
+ background-color: var(--loading-bg);
+}
+
+.loading:after {
+ content: '';
+ position: fixed;
+ z-index: 101;
+ left: 50%;
+ top: 50%;
+ width: 40px;
+ height: 40px;
+ margin-top: -20px;
+ margin-left: -20px;
+ background-image: url('data:image/svg+xml;charset=utf-8,%3Csvg%20xmlns%3D%22http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%22%20viewBox%3D%220%200%2047.6%2047.6%22%20height%3D%22100%25%22%20width%3D%22100%25%22%3E%3Cpath%20opacity%3D%22.235%22%20fill%3D%22%23979797%22%20d%3D%22M44.4%2011.9l-5.2%203c1.5%202.6%202.4%205.6%202.4%208.9%200%209.8-8%2017.8-17.8%2017.8-6.6%200-12.3-3.6-15.4-8.9l-5.2%203C7.3%2042.8%2015%2047.6%2023.8%2047.6c13.1%200%2023.8-10.7%2023.8-23.8%200-4.3-1.2-8.4-3.2-11.9z%22%2F%3E%3Cpath%20fill%3D%22%2366b574%22%20d%3D%22M3.2%2035.7C0%2030.2-.8%2023.8.8%2017.6%202.5%2011.5%206.4%206.4%2011.9%203.2%2017.4%200%2023.8-.8%2030%20.8c6.1%201.6%2011.3%205.6%2014.4%2011.1l-5.2%203c-2.4-4.1-6.2-7.1-10.8-8.3C23.8%205.4%2019%206%2014.9%208.4s-7.1%206.2-8.3%2010.8c-1.2%204.6-.6%209.4%201.8%2013.5l-5.2%203z%22%2F%3E%3C%2Fsvg%3E');
+ will-change: transform;
+ animation: clockwise 2s linear infinite;
+}
+
+@keyframes opacity {
+ 0% {
+ opacity: 0;
+ }
+
+ 100% {
+ opacity: 0.8;
+ }
+}
+
+@keyframes clockwise {
+ 0% {
+ transform: rotate(0deg);
+ }
+
+ 100% {
+ transform: rotate(360deg);
+ }
+}
diff --git a/client_v2/src/components/ui/Loading.tsx b/client_v2/src/components/ui/Loading.tsx
new file mode 100644
index 00000000..a88faefb
--- /dev/null
+++ b/client_v2/src/components/ui/Loading.tsx
@@ -0,0 +1,17 @@
+import React from 'react';
+import classNames from 'clsx';
+import { useTranslation } from 'react-i18next';
+import './Loading.css';
+
+interface LoadingProps {
+ className?: string;
+ text?: string;
+}
+
+const Loading = ({ className, text }: LoadingProps) => {
+ const { t } = useTranslation();
+
+ return {t(text)}
;
+};
+
+export default Loading;
diff --git a/client_v2/src/components/ui/PageTitle.css b/client_v2/src/components/ui/PageTitle.css
new file mode 100644
index 00000000..57966d75
--- /dev/null
+++ b/client_v2/src/components/ui/PageTitle.css
@@ -0,0 +1,38 @@
+.page-header {
+ flex-direction: column;
+ align-items: flex-start;
+}
+
+.page-header--logs {
+ flex-direction: row;
+ align-items: flex-end;
+ margin: 0.5rem 0 2.8rem;
+}
+
+.page-header--logs .page-title {
+ display: inline-flex;
+ align-items: center;
+}
+
+@media (max-width: 991px) {
+ .page-header--logs {
+ flex-direction: column;
+ align-items: center;
+ margin-bottom: 0 0 1.1rem;
+ }
+
+ .page-header--logs .page-title {
+ margin-bottom: 1.1rem;
+ font-size: 1.8rem;
+ }
+}
+
+.page-subtitle {
+ margin-left: 0;
+ font-size: 0.9rem;
+}
+
+.page-title--large {
+ font-size: 36px;
+ line-height: 46px;
+}
diff --git a/client_v2/src/components/ui/PageTitle.tsx b/client_v2/src/components/ui/PageTitle.tsx
new file mode 100644
index 00000000..fdbfe959
--- /dev/null
+++ b/client_v2/src/components/ui/PageTitle.tsx
@@ -0,0 +1,23 @@
+import React from 'react';
+
+import './PageTitle.css';
+
+interface PageTitleProps {
+ title: string;
+ subtitle?: string;
+ children?: React.ReactNode;
+ containerClass?: string;
+}
+
+const PageTitle = ({ title, subtitle, children, containerClass }: PageTitleProps) => (
+
+
+
{title}
+ {children}
+
+
+ {subtitle &&
{subtitle}
}
+
+);
+
+export default PageTitle;
diff --git a/client_v2/src/containers/Settings.ts b/client_v2/src/containers/Settings.ts
new file mode 100644
index 00000000..21e4db8d
--- /dev/null
+++ b/client_v2/src/containers/Settings.ts
@@ -0,0 +1,38 @@
+import { connect } from 'react-redux';
+
+import { initSettings, toggleSetting } from '../actions';
+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';
+
+import { Settings } from '../components/Settings';
+
+const mapStateToProps = (state: any) => {
+ const { settings, services, stats, queryLogs, filtering } = state;
+ const props = {
+ settings,
+ services,
+ stats,
+ queryLogs,
+ filtering,
+ };
+ return props;
+};
+
+const mapDispatchToProps = {
+ initSettings,
+ toggleSetting,
+ getBlockedServices,
+ updateBlockedServices,
+ getStatsConfig,
+ setStatsConfig,
+ resetStats,
+ clearLogs,
+ getLogsConfig,
+ setLogsConfig,
+ getFilteringStatus,
+ setFiltersConfig,
+};
+
+export default connect(mapStateToProps, mapDispatchToProps)(Settings);
diff --git a/client_v2/src/helpers/constants.ts b/client_v2/src/helpers/constants.ts
index ed22d820..4acab2ec 100644
--- a/client_v2/src/helpers/constants.ts
+++ b/client_v2/src/helpers/constants.ts
@@ -476,7 +476,7 @@ export const TOAST_TYPES = {
export const SUCCESS_TOAST_TIMEOUT = 5000;
export const ONE_SECOND_IN_MS = 1000;
-export const FAILURE_TOAST_TIMEOUT = 30000;
+export const FAILURE_TOAST_TIMEOUT = 5000;
export const TOAST_TIMEOUTS = {
[TOAST_TYPES.SUCCESS]: SUCCESS_TOAST_TIMEOUT,
diff --git a/client_v2/src/initialState.ts b/client_v2/src/initialState.ts
index b1378c5b..7c32c046 100644
--- a/client_v2/src/initialState.ts
+++ b/client_v2/src/initialState.ts
@@ -213,6 +213,7 @@ export type StatsData = {
enabled: boolean;
topUpstreamsAvgTime: { name: string; count: number }[];
topUpstreamsResponses: { name: string; count: number }[];
+ ignored: string[];
};
export type ClientsData = {
@@ -365,6 +366,7 @@ export type QueryLogsData = {
isDetailed: boolean;
isEntireLog: boolean;
customInterval: any;
+ ignored: string[];
};
export type ServicesData = {
@@ -566,6 +568,7 @@ export const initialState: RootState = {
isDetailed: true,
isEntireLog: false,
customInterval: null,
+ ignored: [],
},
rewrites: {
processing: true,
@@ -612,6 +615,7 @@ export const initialState: RootState = {
enabled: true,
topUpstreamsAvgTime: [],
topUpstreamsResponses: [],
+ ignored: [],
},
toasts: { notices: [] },
};
diff --git a/client_v2/src/lib/theme/Form.module.pcss b/client_v2/src/lib/theme/Form.module.pcss
new file mode 100644
index 00000000..1814dde6
--- /dev/null
+++ b/client_v2/src/lib/theme/Form.module.pcss
@@ -0,0 +1,20 @@
+.buttonGroup {
+ display: flex;
+ align-items: center;
+ flex-direction: column;
+ gap: 16px;
+ padding: 8px 16px;
+
+ @media (min-width: 768px) {
+ flex-direction: row;
+ }
+
+ .button {
+ flex: 1;
+ min-width: 200px;
+ }
+}
+
+.input {
+ padding: 8px 0;
+}
diff --git a/client_v2/src/lib/theme/Layout.module.pcss b/client_v2/src/lib/theme/Layout.module.pcss
index a41cbe4a..6c2c7a18 100644
--- a/client_v2/src/lib/theme/Layout.module.pcss
+++ b/client_v2/src/lib/theme/Layout.module.pcss
@@ -1,37 +1,29 @@
.container {
margin: 0 16px;
- padding: 24px 0;
+ padding: 40px 0;
height: 100%;
min-height: 100vh;
color: var(--default-main-text);
- @media (min-width: 768px) {
- padding: 48px 24px 80px;
-
- &[data-height='true'] {
- min-height: 70vh;
- }
- }
-
- @media (min-width: 1200px) {
- max-width: 1164px;
- margin: 0 auto;
-
- &[data-height='true'] {
- min-height: 100vh;
- }
- }
-
- @media (min-width: 1400px) {
- min-width: 0;
- width: 100%;
- }
-
- &_update {
+ @media (min-width: 1024px) {
margin: 0;
+ padding: 48px 24px 72px;
+ max-width: 632px;
+ }
- @media (min-width: 1400px) {
- margin: 0 auto;
- }
+ &_full {
+ max-width: 100%;
+ }
+}
+
+.title {
+ padding: 0 16px;
+}
+
+.subtitle {
+ padding: 40px 16px 16px 16px;
+
+ @media (min-width: 1024px) {
+ padding-top: 32px;
}
}
diff --git a/client_v2/src/lib/theme/Link.module.pcss b/client_v2/src/lib/theme/Link.module.pcss
index a9ef24a1..7ea6022f 100644
--- a/client_v2/src/lib/theme/Link.module.pcss
+++ b/client_v2/src/lib/theme/Link.module.pcss
@@ -36,7 +36,7 @@
}
}
- &.no_decoration {
+ &.noDecoration {
text-decoration: none;
}
}
diff --git a/client_v2/src/lib/theme/index.ts b/client_v2/src/lib/theme/index.ts
index 8255e80b..fe73da87 100644
--- a/client_v2/src/lib/theme/index.ts
+++ b/client_v2/src/lib/theme/index.ts
@@ -6,6 +6,7 @@ import title from './Title.module.pcss';
import text from './Text.module.pcss';
import dropdown from './Dropdown.module.pcss';
import select from './Select.module.pcss';
+import form from './Form.module.pcss';
const theme = {
link,
@@ -16,6 +17,7 @@ const theme = {
text,
dropdown,
select,
+ form,
};
export default theme;