Pull request #2231: ADG-8368 Frontend rewritten in TypeScript, added Node 18 support

Merge in DNS/adguard-home from ADG-8368-typescript-node-18 to master

Squashed commit of the following:

commit daa288ae0d76178af24595cc807055902e6f09ab
Merge: 4c89cf720 1085d59a6
Author: Igor Lobanov <bniwredyc@gmail.com>
Date:   Mon Jun 10 17:22:20 2024 +0200

    merge

commit 4c89cf7209
Author: Ildar Kamalov <ik@adguard.com>
Date:   Thu Jun 6 13:27:18 2024 +0300

    remove install from initial state

commit b943f2011f
Author: Igor Lobanov <bniwredyc@gmail.com>
Date:   Wed Jun 5 23:10:55 2024 +0200

    frontend production build fix

commit cd1be2d66d
Author: Igor Lobanov <bniwredyc@gmail.com>
Date:   Wed Jun 5 20:23:14 2024 +0200

    production build quickfix

commit 7b8ac01fc2
Author: Ainar Garipov <A.Garipov@AdGuard.COM>
Date:   Wed Jun 5 19:57:31 2024 +0300

    all: upd node docker

commit 02afed66d5
Author: Igor Lobanov <bniwredyc@gmail.com>
Date:   Wed Jun 5 18:23:12 2024 +0200

    changelog fixes

commit 9c0f736f0c
Merge: 62c4fbf1e e04775c4f
Author: Igor Lobanov <bniwredyc@gmail.com>
Date:   Wed Jun 5 18:18:29 2024 +0200

    merge

commit 62c4fbf1e3
Author: Igor Lobanov <bniwredyc@gmail.com>
Date:   Wed Jun 5 16:22:22 2024 +0200

    empty line in changelog

commit 76b1e44a93
Author: Igor Lobanov <bniwredyc@gmail.com>
Date:   Wed Jun 5 16:20:37 2024 +0200

    changelog

commit f783e90040
Author: Igor Lobanov <bniwredyc@gmail.com>
Date:   Wed Jun 5 16:19:13 2024 +0200

    filters.js -> filters.ts

commit 3d4ce6554c
Author: Igor Lobanov <bniwredyc@gmail.com>
Date:   Wed Jun 5 16:18:03 2024 +0200

    generated file removed

commit e35ba58f2a
Author: Igor Lobanov <bniwredyc@gmail.com>
Date:   Wed Jun 5 15:45:21 2024 +0200

    rollback unwanted changes

commit 1f30d4216d
Author: Igor Lobanov <bniwredyc@gmail.com>
Date:   Wed Jun 5 15:27:36 2024 +0200

    review fix

commit 6cd4e44f07
Author: Igor Lobanov <bniwredyc@gmail.com>
Date:   Wed Jun 5 11:55:39 2024 +0200

    missing generated file restoresd

commit 2ab738b303
Author: Igor Lobanov <bniwredyc@gmail.com>
Date:   Wed Jun 5 11:40:32 2024 +0200

    Frontend rewritten in TypeScript, added Node 18 support
This commit is contained in:
Igor Lobanov
2024-06-10 18:42:23 +03:00
parent 1085d59a65
commit 1afe226ce8
296 changed files with 32122 additions and 32651 deletions

View File

@@ -1,32 +0,0 @@
.accordion {
color: #495057;
}
.accordion__label {
position: relative;
display: inline-block;
padding-left: 25px;
cursor: pointer;
user-select: none;
}
.accordion__label:after {
content: "";
position: absolute;
top: 7px;
left: 0;
width: 17px;
height: 10px;
background-image: url("./svg/chevron-down.svg");
background-repeat: no-repeat;
background-position: center;
background-size: 100%;
}
.accordion__label--open:after {
transform: rotate(180deg);
}
.accordion__content {
padding-top: 5px;
}

View File

@@ -56,7 +56,7 @@
}
.card-body--loading:before {
content: "";
content: '';
position: absolute;
top: 0;
left: 0;
@@ -67,7 +67,7 @@
}
.card-body--loading:after {
content: "";
content: '';
position: absolute;
z-index: 101;
left: 50%;
@@ -76,7 +76,7 @@
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");
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;
}
@@ -113,7 +113,7 @@
}
.card-value-percent:after {
content: "%";
content: '%';
}
.card--full {
@@ -150,6 +150,6 @@
background-color: #ecf7ff;
}
[data-theme="dark"] .card .logs__row--blue {
[data-theme='dark'] .card .logs__row--blue {
background-color: var(--logs__row--blue-bgcolor);
}

View File

@@ -1,50 +0,0 @@
import React from 'react';
import PropTypes from 'prop-types';
import './Card.css';
const Card = ({
type, id, title, subtitle, refresh, bodyType, children,
}) => (
<div className={type ? `card ${type}` : 'card'} id={id || ''}>
{(title || subtitle) && (
<div className="card-header with-border">
<div className="card-inner">
{title && (
<div className="card-title">
{title}
</div>
)}
{subtitle && (
<div
className="card-subtitle"
dangerouslySetInnerHTML={{ __html: subtitle }}
/>
)}
</div>
{refresh && (
<div className="card-options">
{refresh}
</div>
)}
</div>
)}
<div className={bodyType || 'card-body'}>
{children}
</div>
</div>
);
Card.propTypes = {
id: PropTypes.string,
title: PropTypes.string,
subtitle: PropTypes.string,
bodyType: PropTypes.string,
type: PropTypes.string,
refresh: PropTypes.node,
children: PropTypes.node.isRequired,
};
export default Card;

View File

@@ -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) => (
<div className={type ? `card ${type}` : 'card'} id={id || ''}>
{(title || subtitle) && (
<div className="card-header with-border">
<div className="card-inner">
{title && <div className="card-title">{title}</div>}
{subtitle && <div className="card-subtitle" dangerouslySetInnerHTML={{ __html: subtitle }} />}
</div>
{refresh && <div className="card-options">{refresh}</div>}
</div>
)}
<div className={bodyType || 'card-body'}>{children}</div>
</div>
);
export default Card;

View File

@@ -1,28 +1,27 @@
import React from 'react';
import PropTypes from 'prop-types';
import LogsSearchLink from './LogsSearchLink';
import { formatNumber } from '../../helpers/helpers';
const Cell = ({
value,
percent,
color,
search,
}) => (
interface CellProps {
value: number;
percent: number;
color: string;
search?: string;
onSearchRedirect?: (...args: unknown[]) => string;
}
const Cell = ({ value, percent, color, search }: CellProps) => (
<div className="stats__row">
<div className="stats__row-value mb-1">
<strong>
{search ? (
<LogsSearchLink search={search}>
{formatNumber(value)}
</LogsSearchLink>
) : (
formatNumber(value)
)}
{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"
@@ -35,12 +34,4 @@ const Cell = ({
</div>
);
Cell.propTypes = {
value: PropTypes.number.isRequired,
percent: PropTypes.number.isRequired,
color: PropTypes.string.isRequired,
search: PropTypes.string,
onSearchRedirect: PropTypes.func,
};
export default Cell;

View File

@@ -1,7 +1,12 @@
import React from 'react';
import PropTypes from 'prop-types';
const CellWrap = ({ value }, formatValue, formatTitle = formatValue) => {
interface CellWrapProps {
value?: string | number;
formatValue?: (...args: unknown[]) => unknown;
formatTitle?: (...args: unknown[]) => unknown;
}
const CellWrap = ({ value }: CellWrapProps, formatValue?: any, formatTitle = formatValue) => {
if (!value) {
return '';
}
@@ -17,13 +22,4 @@ const CellWrap = ({ value }, formatValue, formatTitle = formatValue) => {
);
};
CellWrap.propTypes = {
value: PropTypes.oneOfType([
PropTypes.string,
PropTypes.number,
]),
formatValue: PropTypes.func,
formatTitle: PropTypes.func,
};
export default CellWrap;

View File

@@ -39,7 +39,7 @@
}
.checkbox__label:before {
content: "";
content: '';
position: relative;
top: 1px;
display: inline-block;
@@ -53,7 +53,9 @@
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;
transition:
0.3s ease-in-out box-shadow,
0.3s ease-in-out opacity;
}
.checkbox__label .checkbox__label-text {

View File

@@ -1,43 +0,0 @@
import React, { Component } from 'react';
import PropTypes from 'prop-types';
import { withTranslation } from 'react-i18next';
import './Checkbox.css';
class Checkbox extends Component {
render() {
const {
title,
subtitle,
enabled,
handleChange,
disabled,
t,
} = this.props;
return (
<div className="form__group form__group--checkbox">
<label className="checkbox checkbox--settings">
<span className="checkbox__marker"/>
<input type="checkbox" className="checkbox__input" onChange={handleChange} checked={enabled} disabled={disabled}/>
<span className="checkbox__label">
<span className="checkbox__label-text">
<span className="checkbox__label-title">{ t(title) }</span>
<span className="checkbox__label-subtitle" dangerouslySetInnerHTML={{ __html: t(subtitle) }}/>
</span>
</span>
</label>
</div>
);
}
}
Checkbox.propTypes = {
title: PropTypes.string.isRequired,
subtitle: PropTypes.string.isRequired,
enabled: PropTypes.bool.isRequired,
handleChange: PropTypes.func.isRequired,
disabled: PropTypes.bool,
t: PropTypes.func,
};
export default withTranslation()(Checkbox);

View File

@@ -0,0 +1,59 @@
import React, { Component } from 'react';
import { withTranslation } from 'react-i18next';
import './Checkbox.css';
interface CheckboxProps {
title: string;
subtitle: string;
enabled: boolean;
handleChange: (...args: unknown[]) => unknown;
disabled?: boolean;
t?: (...args: unknown[]) => string;
}
class Checkbox extends Component<CheckboxProps> {
render() {
const {
title,
subtitle,
enabled,
handleChange,
disabled,
t,
} = this.props;
return (
<div className="form__group form__group--checkbox">
<label className="checkbox checkbox--settings">
<span className="checkbox__marker" />
<input
type="checkbox"
className="checkbox__input"
onChange={handleChange}
checked={enabled}
disabled={disabled}
/>
<span className="checkbox__label">
<span className="checkbox__label-text">
<span className="checkbox__label-title">{t(title)}</span>
<span
className="checkbox__label-subtitle"
dangerouslySetInnerHTML={{ __html: t(subtitle) }}
/>
</span>
</span>
</label>
</div>
);
}
}
export default withTranslation()(Checkbox);

View File

@@ -1,12 +1,25 @@
import React, { Component } from 'react';
import PropTypes from 'prop-types';
import classnames from 'classnames';
import { withTranslation } from 'react-i18next';
import enhanceWithClickOutside from 'react-click-outside';
import './Dropdown.css';
class Dropdown extends Component {
type DropdownProps = {
label: string;
children: React.ReactNode;
controlClassName: string;
menuClassName: string;
baseClassName: string;
icon?: string;
}
type DropdownState = {
isOpen: boolean;
}
class Dropdown extends Component<DropdownProps, DropdownState> {
state = {
isOpen: false,
};
@@ -29,8 +42,8 @@ class Dropdown extends Component {
const {
label,
controlClassName,
menuClassName,
baseClassName,
menuClassName = 'dropdown-menu dropdown-menu-arrow',
baseClassName = 'dropdown',
icon,
children,
} = this.props;
@@ -51,11 +64,7 @@ class Dropdown extends Component {
return (
<div className={dropdownClass}>
<a
className={controlClassName}
aria-expanded={ariaSettings}
onClick={this.toggleDropdown}
>
<a className={controlClassName} aria-expanded={ariaSettings} onClick={this.toggleDropdown}>
{icon && (
<svg className="nav-icon">
<use xlinkHref={`#${icon}`} />
@@ -63,6 +72,7 @@ class Dropdown extends Component {
)}
{label}
</a>
<div className={dropdownMenuClass} onClick={this.hideDropdown}>
{children}
</div>
@@ -71,19 +81,4 @@ class Dropdown extends Component {
}
}
Dropdown.defaultProps = {
baseClassName: 'dropdown',
menuClassName: 'dropdown-menu dropdown-menu-arrow',
controlClassName: '',
};
Dropdown.propTypes = {
label: PropTypes.string.isRequired,
children: PropTypes.node.isRequired,
controlClassName: PropTypes.node.isRequired,
menuClassName: PropTypes.string.isRequired,
baseClassName: PropTypes.string.isRequired,
icon: PropTypes.string,
};
export default withTranslation()(enhanceWithClickOutside(Dropdown));

View File

@@ -3,8 +3,10 @@ import { Trans } from 'react-i18next';
import isAfter from 'date-fns/is_after';
import addDays from 'date-fns/add_days';
import { useSelector } from 'react-redux';
import Topline from './Topline';
import { EMPTY_DATE } from '../../helpers/constants';
import { RootState } from '../../initialState';
const EXPIRATION_ENUM = {
VALID: 'VALID',
@@ -23,7 +25,7 @@ const EXPIRATION_STATE = {
},
};
const getExpirationFlags = (not_after) => {
const getExpirationFlags = (not_after: any) => {
const DAYS_BEFORE_EXPIRATION = 5;
const now = Date.now();
@@ -36,7 +38,7 @@ const getExpirationFlags = (not_after) => {
};
};
const getExpirationEnumKey = (not_after) => {
const getExpirationEnumKey = (not_after: any) => {
const { isExpiring, isExpired } = getExpirationFlags(not_after);
if (isExpired) {
@@ -51,7 +53,7 @@ const getExpirationEnumKey = (not_after) => {
};
const EncryptionTopline = () => {
const not_after = useSelector((state) => state.encryption.not_after);
const not_after = useSelector((state: RootState) => state.encryption.not_after);
if (not_after === EMPTY_DATE) {
return null;
@@ -66,11 +68,16 @@ const EncryptionTopline = () => {
const { toplineType, i18nKey } = EXPIRATION_STATE[expirationStateKey];
return (
<Topline type={toplineType}>
<Trans components={[<a href="#encryption" key="0">link</a>]}>
{i18nKey}
</Trans>
</Topline>
<Topline type={toplineType}>
<Trans
components={[
<a href="#encryption" key="0">
link
</a>,
]}>
{i18nKey}
</Trans>
</Topline>
);
};

View File

@@ -73,7 +73,7 @@
}
.btn-secondary.footer__theme-button,
[data-theme="dark"] .btn-secondary.footer__theme-button {
[data-theme='dark'] .btn-secondary.footer__theme-button {
height: 38px;
border-color: var(--ctrl-select-bgcolor);
}
@@ -86,12 +86,12 @@
color: var(--gray-ac);
}
[data-theme="dark"] .footer__theme-icon {
[data-theme='dark'] .footer__theme-icon {
color: var(--mcolor);
}
.footer__theme-icon--active,
[data-theme="dark"] .footer__theme-icon--active {
[data-theme='dark'] .footer__theme-icon--active {
color: var(--btn-success-bgcolor);
}

View File

@@ -10,8 +10,11 @@ import i18n from '../../i18n';
import Version from './Version';
import './Footer.css';
import './Select.css';
import { setHtmlLangAttr, setUITheme } from '../../helpers/helpers';
import { changeTheme } from '../../actions';
import { RootState } from '../../initialState';
const linksData = [
{
@@ -33,12 +36,8 @@ const Footer = () => {
const { t } = useTranslation();
const dispatch = useDispatch();
const currentTheme = useSelector((state) => (
state.dashboard ? state.dashboard.theme : THEMES.auto
));
const profileName = useSelector((state) => (
state.dashboard ? state.dashboard.name : ''
));
const currentTheme = useSelector((state: RootState) => (state.dashboard ? state.dashboard.theme : THEMES.auto));
const profileName = useSelector((state: RootState) => (state.dashboard ? state.dashboard.name : ''));
const isLoggedIn = profileName !== '';
const [currentThemeLocal, setCurrentThemeLocal] = useState(THEMES.auto);
@@ -47,13 +46,13 @@ const Footer = () => {
return today.getFullYear();
};
const changeLanguage = (event) => {
const changeLanguage = (event: any) => {
const { value } = event.target;
i18n.changeLanguage(value);
setHtmlLangAttr(value);
};
const onThemeChange = (value) => {
const onThemeChange = (value: any) => {
if (isLoggedIn) {
dispatch(changeTheme(value));
} else {
@@ -62,22 +61,31 @@ const Footer = () => {
}
};
const renderCopyright = () => <div className="footer__column">
<div className="footer__copyright">
{t('copyright')} &copy; {getYear()}{' '}
<a target="_blank" rel="noopener noreferrer" href="https://link.adtidy.org/forward.html?action=home&from=ui&app=home">AdGuard</a>
const renderCopyright = () => (
<div className="footer__column">
<div className="footer__copyright">
{t('copyright')} &copy; {getYear()}{' '}
<a
target="_blank"
rel="noopener noreferrer"
href="https://link.adtidy.org/forward.html?action=home&from=ui&app=home">
AdGuard
</a>
</div>
</div>
</div>;
);
const renderLinks = (linksData) => linksData.map(({ name, href, className = '' }) => <a
key={name}
href={href}
className={cn('footer__link', className)}
target="_blank"
rel="noopener noreferrer"
>
{t(name)}
</a>);
const renderLinks = (linksData: any) =>
linksData.map(({ name, href, className = '' }: any) => (
<a
key={name}
href={href}
className={cn('footer__link', className)}
target="_blank"
rel="noopener noreferrer">
{t(name)}
</a>
));
const renderThemeButtons = () => {
const currentValue = isLoggedIn ? currentTheme : currentThemeLocal;
@@ -97,27 +105,20 @@ const Footer = () => {
},
};
return (
Object.values(THEMES)
.map((theme) => (
<button
key={theme}
type="button"
className="btn btn-sm btn-secondary footer__theme-button"
onClick={() => onThemeChange(theme)}
title={content[theme].desc}
>
<svg
className={cn(
'footer__theme-icon',
{ 'footer__theme-icon--active': currentValue === theme },
)}
>
<use xlinkHref={content[theme].icon} />
</svg>
</button>
))
);
return Object.values(THEMES)
.map((theme: any) => (
<button
key={theme}
type="button"
className="btn btn-sm btn-secondary footer__theme-button"
onClick={() => onThemeChange(theme)}
title={content[theme].desc}>
<svg className={cn('footer__theme-icon', { 'footer__theme-icon--active': currentValue === theme })}>
<use xlinkHref={content[theme].icon} />
</svg>
</button>
));
};
return (
@@ -125,37 +126,35 @@ const Footer = () => {
<footer className="footer">
<div className="container">
<div className="footer__row">
<div className="footer__column footer__column--links">
{renderLinks(linksData)}
</div>
<div className="footer__column footer__column--links">{renderLinks(linksData)}</div>
<div className="footer__column footer__column--theme">
<div className="footer__themes">
<div className="btn-group">
{renderThemeButtons()}
</div>
<div className="btn-group">{renderThemeButtons()}</div>
</div>
</div>
<div className="footer__column footer__column--language">
<select
className="form-control select select--language"
value={i18n.language}
onChange={changeLanguage}
>
{Object.keys(LANGUAGES)
.map((lang) => (
<option key={lang} value={lang}>
{LANGUAGES[lang]}
</option>
))}
onChange={changeLanguage}>
{Object.keys(LANGUAGES).map((lang) => (
<option key={lang} value={lang}>
{LANGUAGES[lang]}
</option>
))}
</select>
</div>
</div>
</div>
</footer>
<div className="footer">
<div className="container">
<div className="footer__row">
{renderCopyright()}
<div className="footer__column footer__column--language">
<Version />
</div>

View File

@@ -1,372 +0,0 @@
import React, { useState } from 'react';
import PropTypes from 'prop-types';
import { Trans, useTranslation } from 'react-i18next';
import i18next from 'i18next';
import { useSelector } from 'react-redux';
import { MOBILE_CONFIG_LINKS } from '../../../helpers/constants';
import Tabs from '../Tabs';
import MobileConfigForm from './MobileConfigForm';
const renderLi = ({ label, components }) => <li key={label}>
<Trans components={components?.map((props) => {
if (React.isValidElement(props)) {
return props;
}
const {
// eslint-disable-next-line react/prop-types
href, target = '_blank', rel = 'noopener noreferrer', key = '0',
} = props;
return <a href={href} target={target} rel={rel} key={key}>link</a>;
})}>
{label}
</Trans>
</li>;
const getDnsPrivacyList = () => [
{
title: 'Android',
list: [
{
label: 'setup_dns_privacy_android_1',
},
{
label: 'setup_dns_privacy_android_2',
components: [
{
key: 0,
href: 'https://link.adtidy.org/forward.html?action=android&from=ui&app=home',
},
<code key="1">text</code>,
],
},
{
label: 'setup_dns_privacy_android_3',
components: [
{
key: 0,
href: 'https://getintra.org/',
},
<code key="1">text</code>,
],
},
],
},
{
title: 'iOS',
list: [
{
label: 'setup_dns_privacy_ios_2',
components: [
{
key: 0,
href: 'https://link.adtidy.org/forward.html?action=ios&from=ui&app=home',
},
<code key="1">text</code>,
],
},
{
label: 'setup_dns_privacy_ios_1',
components: [
{
key: 0,
href: 'https://itunes.apple.com/app/id1452162351',
},
<code key="1">text</code>,
{
key: 2,
href: 'https://dnscrypt.info/stamps',
},
],
},
],
},
{
title: 'setup_dns_privacy_other_title',
list: [
{
label: 'setup_dns_privacy_other_1',
},
{
label: 'setup_dns_privacy_other_2',
components: [
{
key: 0,
href: 'https://github.com/AdguardTeam/dnsproxy',
},
],
},
{
href: 'https://github.com/jedisct1/dnscrypt-proxy',
label: 'setup_dns_privacy_other_3',
components: [
{
key: 0,
href: 'https://github.com/jedisct1/dnscrypt-proxy',
},
<code key="1">text</code>,
],
},
{
label: 'setup_dns_privacy_other_4',
components: [
{
key: 0,
href: 'https://support.mozilla.org/kb/firefox-dns-over-https',
},
<code key="1">text</code>,
],
},
{
label: 'setup_dns_privacy_other_5',
components: [
{
key: 0,
href: 'https://dnscrypt.info/implementations',
},
{
key: 1,
href: 'https://dnsprivacy.org/wiki/display/DP/DNS+Privacy+Clients',
},
],
},
],
},
];
const renderDnsPrivacyList = ({ title, list }) => (
<div className="tab__paragraph" key={title}>
<strong>
<Trans>{title}</Trans>
</strong>
<ul>
{list.map(({ label, components, renderComponent = renderLi }) => (
renderComponent({ label, components })
))}
</ul>
</div>
);
const getTabs = ({
tlsAddress,
httpsAddress,
showDnsPrivacyNotice,
serverName,
portHttps,
t,
}) => ({
Router: {
// eslint-disable-next-line react/display-name
getTitle: () => <p>
<Trans>install_devices_router_desc</Trans>
</p>,
title: 'Router',
list: ['install_devices_router_list_1',
'install_devices_router_list_2',
'install_devices_router_list_3',
// eslint-disable-next-line react/jsx-key
<Trans components={[
<a href="#dhcp" key="0">
link
</a>,
]}>install_devices_router_list_4</Trans>,
],
},
Windows: {
title: 'Windows',
list: ['install_devices_windows_list_1',
'install_devices_windows_list_2',
'install_devices_windows_list_3',
'install_devices_windows_list_4',
'install_devices_windows_list_5',
'install_devices_windows_list_6'],
},
macOS: {
title: 'macOS',
list: ['install_devices_macos_list_1',
'install_devices_macos_list_2',
'install_devices_macos_list_3',
'install_devices_macos_list_4'],
},
Android: {
title: 'Android',
list: ['install_devices_android_list_1',
'install_devices_android_list_2',
'install_devices_android_list_3',
'install_devices_android_list_4',
'install_devices_android_list_5'],
},
iOS: {
title: 'iOS',
list: ['install_devices_ios_list_1',
'install_devices_ios_list_2',
'install_devices_ios_list_3',
'install_devices_ios_list_4'],
},
dns_privacy: {
title: 'dns_privacy',
getTitle: function Title() {
return <div label="dns_privacy" title={t('dns_privacy')}>
<div className="tab__text">
{tlsAddress?.length > 0 && (
<div className="tab__paragraph">
<Trans
values={{ address: tlsAddress[0] }}
components={[
<strong key="0">text</strong>,
<code key="1">text</code>,
]}
>
setup_dns_privacy_1
</Trans>
</div>
)}
{httpsAddress?.length > 0 && (
<div className="tab__paragraph">
<Trans
values={{ address: httpsAddress[0] }}
components={[
<strong key="0">text</strong>,
<code key="1">text</code>,
]}
>
setup_dns_privacy_2
</Trans>
</div>
)}
{showDnsPrivacyNotice ? (
<div className="tab__paragraph">
<Trans
components={[
<a
href="https://github.com/AdguardTeam/AdguardHome/wiki/Encryption"
target="_blank"
rel="noopener noreferrer"
key="0"
>
link
</a>,
<code key="1">text</code>,
]}
>
setup_dns_notice
</Trans>
</div>
) : (
<>
<div className="tab__paragraph">
<Trans components={[<p key="0">text</p>]}>
setup_dns_privacy_3
</Trans>
</div>
{getDnsPrivacyList().map(renderDnsPrivacyList)}
<div>
<strong>
<Trans>
setup_dns_privacy_ioc_mac
</Trans>
</strong>
</div>
<div className="mb-3">
<Trans components={{ highlight: <code /> }}>
setup_dns_privacy_4
</Trans>
</div>
<MobileConfigForm
initialValues={{
host: serverName,
clientId: '',
protocol: MOBILE_CONFIG_LINKS.DOH,
port: portHttps,
}}
/>
</>
)}
</div>
</div>;
},
},
});
const renderContent = ({ title, list, getTitle }) => (
<div key={title} label={i18next.t(title)}>
<div className="tab__title">
{i18next.t(title)}
</div>
<div className="tab__text">
{getTitle?.()}
{list && (
<ol>
{list.map((item) => (
<li key={item}>
<Trans>{item}</Trans>
</li>
))}
</ol>
)}
</div>
</div>
);
const Guide = ({ dnsAddresses }) => {
const { t } = useTranslation();
const serverName = useSelector((state) => state.encryption?.server_name);
const portHttps = useSelector((state) => state.encryption?.port_https);
const tlsAddress = dnsAddresses?.filter((item) => item.includes('tls://')) ?? '';
const httpsAddress = dnsAddresses?.filter((item) => item.includes('https://')) ?? '';
const showDnsPrivacyNotice = httpsAddress.length < 1 && tlsAddress.length < 1;
const [activeTabLabel, setActiveTabLabel] = useState('Router');
const tabs = getTabs({
tlsAddress,
httpsAddress,
showDnsPrivacyNotice,
serverName,
portHttps,
t,
});
const activeTab = renderContent(tabs[activeTabLabel]);
return (
<div>
<Tabs
tabs={tabs}
activeTabLabel={activeTabLabel}
setActiveTabLabel={setActiveTabLabel}
>
{activeTab}
</Tabs>
</div>
);
};
Guide.defaultProps = {
dnsAddresses: [],
};
Guide.propTypes = {
dnsAddresses: PropTypes.array,
};
renderDnsPrivacyList.propTypes = {
title: PropTypes.string.isRequired,
list: PropTypes.array.isRequired,
renderList: PropTypes.func,
};
renderContent.propTypes = {
title: PropTypes.string.isRequired,
list: PropTypes.array.isRequired,
getTitle: PropTypes.func,
};
renderLi.propTypes = {
label: PropTypes.string,
components: PropTypes.string,
};
export default Guide;

View File

@@ -0,0 +1,385 @@
import React, { useState } from 'react';
import { Trans, useTranslation } from 'react-i18next';
import i18next from 'i18next';
import { useSelector } from 'react-redux';
import { MOBILE_CONFIG_LINKS } from '../../../helpers/constants';
import Tabs from '../Tabs';
import MobileConfigForm from './MobileConfigForm';
import { RootState } from '../../../initialState';
interface renderLiProps {
label?: string;
components?: JSX.Element[];
}
const renderLi = ({ label, components }: renderLiProps) => (
<li key={label}>
<Trans
components={components?.map((props: any) => {
if (React.isValidElement(props)) {
return props;
}
const {
// eslint-disable-next-line react/prop-types
href,
target = '_blank',
rel = 'noopener noreferrer',
key = '0',
} = props;
return (
<a href={href} target={target} rel={rel} key={key}>
link
</a>
);
})}>
{label}
</Trans>
</li>
);
const getDnsPrivacyList = () => [
{
title: 'Android',
list: [
{
label: 'setup_dns_privacy_android_1',
},
{
label: 'setup_dns_privacy_android_2',
components: [
{
key: 0,
href: 'https://link.adtidy.org/forward.html?action=android&from=ui&app=home',
},
<code key="1">text</code>,
],
},
{
label: 'setup_dns_privacy_android_3',
components: [
{
key: 0,
href: 'https://getintra.org/',
},
<code key="1">text</code>,
],
},
],
},
{
title: 'iOS',
list: [
{
label: 'setup_dns_privacy_ios_2',
components: [
{
key: 0,
href: 'https://link.adtidy.org/forward.html?action=ios&from=ui&app=home',
},
<code key="1">text</code>,
],
},
{
label: 'setup_dns_privacy_ios_1',
components: [
{
key: 0,
href: 'https://itunes.apple.com/app/id1452162351',
},
<code key="1">text</code>,
{
key: 2,
href: 'https://dnscrypt.info/stamps',
},
],
},
],
},
{
title: 'setup_dns_privacy_other_title',
list: [
{
label: 'setup_dns_privacy_other_1',
},
{
label: 'setup_dns_privacy_other_2',
components: [
{
key: 0,
href: 'https://github.com/AdguardTeam/dnsproxy',
},
],
},
{
href: 'https://github.com/jedisct1/dnscrypt-proxy',
label: 'setup_dns_privacy_other_3',
components: [
{
key: 0,
href: 'https://github.com/jedisct1/dnscrypt-proxy',
},
<code key="1">text</code>,
],
},
{
label: 'setup_dns_privacy_other_4',
components: [
{
key: 0,
href: 'https://support.mozilla.org/kb/firefox-dns-over-https',
},
<code key="1">text</code>,
],
},
{
label: 'setup_dns_privacy_other_5',
components: [
{
key: 0,
href: 'https://dnscrypt.info/implementations',
},
{
key: 1,
href: 'https://dnsprivacy.org/wiki/display/DP/DNS+Privacy+Clients',
},
],
},
],
},
];
interface renderDnsPrivacyListProps {
title: string;
list: unknown[];
renderList?: (...args: unknown[]) => string;
}
const renderDnsPrivacyList = ({ title, list }: renderDnsPrivacyListProps) => (
<div className="tab__paragraph" key={title}>
<strong>
<Trans>{title}</Trans>
</strong>
<ul>
{list.map(({ label, components, renderComponent = renderLi }: any) =>
renderComponent({ label, components }),
)}
</ul>
</div>
);
const getTabs = ({ tlsAddress, httpsAddress, showDnsPrivacyNotice, serverName, portHttps, t }: any) => ({
Router: {
// eslint-disable-next-line react/display-name
getTitle: () => (
<p>
<Trans>install_devices_router_desc</Trans>
</p>
),
title: 'Router',
list: [
'install_devices_router_list_1',
'install_devices_router_list_2',
'install_devices_router_list_3',
// eslint-disable-next-line react/jsx-key
<Trans
components={[
<a href="#dhcp" key="0">
link
</a>,
]}>
install_devices_router_list_4
</Trans>,
],
},
Windows: {
title: 'Windows',
list: [
'install_devices_windows_list_1',
'install_devices_windows_list_2',
'install_devices_windows_list_3',
'install_devices_windows_list_4',
'install_devices_windows_list_5',
'install_devices_windows_list_6',
],
},
macOS: {
title: 'macOS',
list: [
'install_devices_macos_list_1',
'install_devices_macos_list_2',
'install_devices_macos_list_3',
'install_devices_macos_list_4',
],
},
Android: {
title: 'Android',
list: [
'install_devices_android_list_1',
'install_devices_android_list_2',
'install_devices_android_list_3',
'install_devices_android_list_4',
'install_devices_android_list_5',
],
},
iOS: {
title: 'iOS',
list: [
'install_devices_ios_list_1',
'install_devices_ios_list_2',
'install_devices_ios_list_3',
'install_devices_ios_list_4',
],
},
dns_privacy: {
title: 'dns_privacy',
getTitle: function Title() {
return (
<div title={t('dns_privacy')}>
<div className="tab__text">
{tlsAddress?.length > 0 && (
<div className="tab__paragraph">
<Trans
values={{ address: tlsAddress[0] }}
components={[<strong key="0">text</strong>, <code key="1">text</code>]}>
setup_dns_privacy_1
</Trans>
</div>
)}
{httpsAddress?.length > 0 && (
<div className="tab__paragraph">
<Trans
values={{ address: httpsAddress[0] }}
components={[<strong key="0">text</strong>, <code key="1">text</code>]}>
setup_dns_privacy_2
</Trans>
</div>
)}
{showDnsPrivacyNotice ? (
<div className="tab__paragraph">
<Trans
components={[
<a
href="https://github.com/AdguardTeam/AdguardHome/wiki/Encryption"
target="_blank"
rel="noopener noreferrer"
key="0">
link
</a>,
<code key="1">text</code>,
]}>
setup_dns_notice
</Trans>
</div>
) : (
<>
<div className="tab__paragraph">
<Trans components={[<p key="0">text</p>]}>setup_dns_privacy_3</Trans>
</div>
{getDnsPrivacyList().map(renderDnsPrivacyList)}
<div>
<strong>
<Trans>setup_dns_privacy_ioc_mac</Trans>
</strong>
</div>
<div className="mb-3">
<Trans components={{ highlight: <code /> }}>setup_dns_privacy_4</Trans>
</div>
<MobileConfigForm
initialValues={{
host: serverName,
clientId: '',
protocol: MOBILE_CONFIG_LINKS.DOH,
port: portHttps,
}}
/>
</>
)}
</div>
</div>
);
},
},
});
interface renderContentProps {
title: string;
list: unknown[];
getTitle?: (...args: unknown[]) => unknown;
}
const renderContent = ({ title, list, getTitle }: renderContentProps) => (
<div title={i18next.t(title)}>
<div className="tab__title">{i18next.t(title)}</div>
<div className="tab__text">
{getTitle?.()}
{list && (
<ol>
{list.map((item: any) => (
<li key={item}>
<Trans>{item}</Trans>
</li>
))}
</ol>
)}
</div>
</div>
);
interface GuideProps {
dnsAddresses?: unknown[];
}
const Guide = ({ dnsAddresses }: GuideProps) => {
const { t } = useTranslation();
const serverName = useSelector((state: RootState) => state.encryption?.server_name);
const portHttps = useSelector((state: RootState) => state.encryption?.port_https);
const tlsAddress = dnsAddresses?.filter((item: any) => item.includes('tls://')) ?? '';
const httpsAddress = dnsAddresses?.filter((item: any) => item.includes('https://')) ?? '';
const showDnsPrivacyNotice = httpsAddress.length < 1 && tlsAddress.length < 1;
const [activeTabLabel, setActiveTabLabel] = useState('Router');
const tabs = getTabs({
tlsAddress,
httpsAddress,
showDnsPrivacyNotice,
serverName,
portHttps,
t,
});
const activeTab = renderContent(tabs[activeTabLabel]);
return (
<div>
<Tabs tabs={tabs} activeTabLabel={activeTabLabel} setActiveTabLabel={setActiveTabLabel}>
{activeTab}
</Tabs>
</div>
);
};
Guide.defaultProps = {
dnsAddresses: [],
};
export default Guide;

View File

@@ -1,43 +1,32 @@
import React from 'react';
import PropTypes from 'prop-types';
import { Trans } from 'react-i18next';
import { useSelector } from 'react-redux';
import { Field, reduxForm } from 'redux-form';
import i18next from 'i18next';
import cn from 'classnames';
import { getPathWithQueryString } from '../../../helpers/helpers';
import {
CLIENT_ID_LINK,
FORM_NAME,
MOBILE_CONFIG_LINKS,
STANDARD_HTTPS_PORT,
} from '../../../helpers/constants';
import {
renderInputField,
renderSelectField,
toNumber,
} from '../../../helpers/form';
import { CLIENT_ID_LINK, FORM_NAME, MOBILE_CONFIG_LINKS, STANDARD_HTTPS_PORT } from '../../../helpers/constants';
import { renderInputField, renderSelectField, toNumber } from '../../../helpers/form';
import {
validateConfigClientId,
validateServerName,
validatePort,
validateIsSafePort,
} from '../../../helpers/validators';
import { RootState } from '../../../initialState';
const getDownloadLink = (host, clientId, protocol, invalid) => {
const getDownloadLink = (host: any, clientId: any, protocol: any, invalid: any) => {
if (!host || invalid) {
return (
<button
type="button"
className="btn btn-success btn-standard btn-large disabled"
>
<button type="button" className="btn btn-success btn-standard btn-large disabled">
<Trans>download_mobileconfig</Trans>
</button>
);
}
const linkParams = { host };
const linkParams: { host: string, client_id?: string } = { host };
if (clientId) {
linkParams.client_id = clientId;
@@ -47,39 +36,33 @@ const getDownloadLink = (host, clientId, protocol, invalid) => {
<a
href={getPathWithQueryString(protocol, linkParams)}
className={cn('btn btn-success btn-standard btn-large')}
download
>
download>
<Trans>download_mobileconfig</Trans>
</a>
);
};
const MobileConfigForm = ({ invalid }) => {
const formValues = useSelector((state) => state.form[FORM_NAME.MOBILE_CONFIG]?.values);
interface MobileConfigFormProps {
invalid: boolean;
}
const MobileConfigForm = ({ invalid }: MobileConfigFormProps) => {
const formValues = useSelector((state: RootState) => state.form[FORM_NAME.MOBILE_CONFIG]?.values);
if (!formValues) {
return null;
}
const {
host, clientId, protocol, port,
} = formValues;
const { host, clientId, protocol, port } = formValues;
const githubLink = (
<a
href={CLIENT_ID_LINK}
target="_blank"
rel="noopener noreferrer"
>
<a href={CLIENT_ID_LINK} target="_blank" rel="noopener noreferrer">
text
</a>
);
const getHostName = () => {
if (port
&& port !== STANDARD_HTTPS_PORT
&& protocol === MOBILE_CONFIG_LINKS.DOH
) {
if (port && port !== STANDARD_HTTPS_PORT && protocol === MOBILE_CONFIG_LINKS.DOH) {
return `${host}:${port}`;
}
@@ -95,6 +78,7 @@ const MobileConfigForm = ({ invalid }) => {
<label htmlFor="host" className="form__label">
{i18next.t('dhcp_table_hostname')}
</label>
<Field
name="host"
type="text"
@@ -109,6 +93,7 @@ const MobileConfigForm = ({ invalid }) => {
<label htmlFor="port" className="form__label">
{i18next.t('encryption_https')}
</label>
<Field
name="port"
type="number"
@@ -122,15 +107,16 @@ const MobileConfigForm = ({ invalid }) => {
)}
</div>
</div>
<div className="form__group form__group--settings">
<label htmlFor="clientId" className="form__label form__label--with-desc">
{i18next.t('client_id')}
</label>
<div className="form__desc form__desc--top">
<Trans components={{ a: githubLink }}>
client_id_desc
</Trans>
<Trans components={{ a: githubLink }}>client_id_desc</Trans>
</div>
<Field
name="clientId"
type="text"
@@ -140,22 +126,16 @@ const MobileConfigForm = ({ invalid }) => {
validate={validateConfigClientId}
/>
</div>
<div className="form__group form__group--settings">
<label htmlFor="protocol" className="form__label">
{i18next.t('protocol')}
</label>
<Field
name="protocol"
type="text"
component={renderSelectField}
className="form-control"
>
<option value={MOBILE_CONFIG_LINKS.DOT}>
{i18next.t('dns_over_tls')}
</option>
<option value={MOBILE_CONFIG_LINKS.DOH}>
{i18next.t('dns_over_https')}
</option>
<Field name="protocol" type="text" component={renderSelectField} className="form-control">
<option value={MOBILE_CONFIG_LINKS.DOT}>{i18next.t('dns_over_tls')}</option>
<option value={MOBILE_CONFIG_LINKS.DOH}>{i18next.t('dns_over_https')}</option>
</Field>
</div>
</div>
@@ -165,8 +145,4 @@ const MobileConfigForm = ({ invalid }) => {
);
};
MobileConfigForm.propTypes = {
invalid: PropTypes.bool.isRequired,
};
export default reduxForm({ form: FORM_NAME.MOBILE_CONFIG })(MobileConfigForm);

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@@ -15,6 +15,6 @@
color: var(--black);
}
.card-chart-bg path[d^="M0,32"] {
.card-chart-bg path[d^='M0,32'] {
transform: translateY(32px);
}

View File

@@ -1,77 +0,0 @@
import React from 'react';
import { ResponsiveLine } from '@nivo/line';
import addDays from 'date-fns/add_days';
import subDays from 'date-fns/sub_days';
import subHours from 'date-fns/sub_hours';
import dateFormat from 'date-fns/format';
import round from 'lodash/round';
import { useSelector } from 'react-redux';
import PropTypes from 'prop-types';
import './Line.css';
import { msToDays, msToHours } from '../../helpers/helpers';
import { TIME_UNITS } from '../../helpers/constants';
const Line = ({
data, color = 'black',
}) => {
const interval = useSelector((state) => state.stats.interval);
const timeUnits = useSelector((state) => state.stats.timeUnits);
return <ResponsiveLine
enableArea
animate
enableSlices="x"
curve="linear"
colors={[color]}
data={data}
theme={{
crosshair: {
line: {
stroke: 'currentColor',
strokeWidth: 1,
strokeOpacity: 0.5,
},
},
}}
xScale={{
type: 'linear',
min: 0,
max: 'auto',
}}
crosshairType="x"
axisLeft={false}
axisBottom={false}
enableGridX={false}
enableGridY={false}
enablePoints={false}
xFormat={(x) => {
if (timeUnits === TIME_UNITS.HOURS) {
const hoursAgo = msToHours(interval) - x - 1;
return dateFormat(subHours(Date.now(), hoursAgo), 'D MMM HH:00');
}
const daysAgo = subDays(Date.now(), msToDays(interval) - 1);
return dateFormat(addDays(daysAgo, x), 'D MMM YYYY');
}}
yFormat={(y) => round(y, 2)}
sliceTooltip={(slice) => {
const { xFormatted, yFormatted } = slice.slice.points[0].data;
return <div className="line__tooltip">
<span className="line__tooltip-text">
<strong>{yFormatted}</strong>
<br />
<small>{xFormatted}</small>
</span>
</div>;
}}
/>;
};
Line.propTypes = {
data: PropTypes.array.isRequired,
color: PropTypes.string,
width: PropTypes.number,
height: PropTypes.number,
};
export default Line;

View File

@@ -0,0 +1,85 @@
import React from 'react';
import { ResponsiveLine } from '@nivo/line';
import addDays from 'date-fns/add_days';
import subDays from 'date-fns/sub_days';
import subHours from 'date-fns/sub_hours';
import dateFormat from 'date-fns/format';
import round from 'lodash/round';
import { useSelector } from 'react-redux';
import './Line.css';
import { msToDays, msToHours } from '../../helpers/helpers';
import { TIME_UNITS } from '../../helpers/constants';
import { RootState } from '../../initialState';
interface LineProps {
data: any[];
color?: string;
width?: number;
height?: number;
}
const Line = ({ data, color = 'black' }: LineProps) => {
const interval = useSelector((state: RootState) => state.stats.interval);
const timeUnits = useSelector((state: RootState) => state.stats.timeUnits);
return (
<ResponsiveLine
enableArea
animate
enableSlices="x"
curve="linear"
colors={[color]}
data={data}
theme={{
crosshair: {
line: {
stroke: 'currentColor',
strokeWidth: 1,
strokeOpacity: 0.5,
},
},
}}
xScale={{
type: 'linear',
min: 0,
max: 'auto',
}}
crosshairType="x"
axisLeft={null}
axisBottom={null}
enableGridX={null}
enableGridY={null}
enablePoints={null}
xFormat={(x: number) => {
if (timeUnits === TIME_UNITS.HOURS) {
const hoursAgo = msToHours(interval) - x - 1;
return dateFormat(subHours(Date.now(), hoursAgo), 'D MMM HH:00');
}
const daysAgo = subDays(Date.now(), msToDays(interval) - 1);
return dateFormat(addDays(daysAgo, x), 'D MMM YYYY');
}}
yFormat={(y: number) => round(y, 2)}
sliceTooltip={(slice) => {
const { xFormatted, yFormatted } = slice.slice.points[0].data;
return (
<div className="line__tooltip">
<span className="line__tooltip-text">
<strong>{yFormatted}</strong>
<br />
<small>{xFormatted}</small>
</span>
</div>
);
}}
/>
);
};
export default Line;

View File

@@ -6,7 +6,7 @@
}
.loading:before {
content: "";
content: '';
position: fixed;
top: 0;
left: 0;
@@ -17,7 +17,7 @@
}
.loading:after {
content: "";
content: '';
position: fixed;
z-index: 101;
left: 50%;
@@ -26,7 +26,7 @@
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");
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;
}

View File

@@ -1,17 +1,17 @@
import React from 'react';
import PropTypes from 'prop-types';
import classNames from 'classnames';
import { useTranslation } from 'react-i18next';
import './Loading.css';
const Loading = ({ className, text }) => {
interface LoadingProps {
className?: string;
text?: string;
}
const Loading = ({ className, text }: LoadingProps) => {
const { t } = useTranslation();
return <div className={classNames('loading', className)}>{t(text)}</div>;
};
Loading.propTypes = {
className: PropTypes.string,
text: PropTypes.string,
};
export default Loading;

View File

@@ -1,37 +0,0 @@
import React from 'react';
import { Link } from 'react-router-dom';
import PropTypes from 'prop-types';
import { useTranslation } from 'react-i18next';
import { getLogsUrlParams } from '../../helpers/helpers';
import { MENU_URLS } from '../../helpers/constants';
const LogsSearchLink = ({
search = '', response_status = '', children, link = MENU_URLS.logs,
}) => {
const { t } = useTranslation();
const to = link === MENU_URLS.logs ? `${MENU_URLS.logs}${getLogsUrlParams(search && `"${search}"`, response_status)}` : link;
return (
<Link
to={to}
tabIndex={0}
title={t('click_to_view_queries')}
aria-label={t('click_to_view_queries')}
>
{children}
</Link>
);
};
LogsSearchLink.propTypes = {
children: PropTypes.oneOfType([
PropTypes.string,
PropTypes.number,
PropTypes.element]).isRequired,
search: PropTypes.string,
response_status: PropTypes.string,
link: PropTypes.string,
};
export default LogsSearchLink;

View File

@@ -0,0 +1,36 @@
import React from 'react';
import { Link } from 'react-router-dom';
import { useTranslation } from 'react-i18next';
import { getLogsUrlParams } from '../../helpers/helpers';
import { MENU_URLS } from '../../helpers/constants';
interface LogsSearchLinkProps {
children: string | number | React.ReactElement;
search?: string;
response_status?: string;
link?: string;
}
const LogsSearchLink = ({
search = '',
response_status = '',
children,
link = MENU_URLS.logs,
}: LogsSearchLinkProps) => {
const { t } = useTranslation();
const to =
link === MENU_URLS.logs
? `${MENU_URLS.logs}${getLogsUrlParams(search && `"${search}"`, response_status)}`
: link;
return (
<Link to={to} tabIndex={0} title={t('click_to_view_queries')} aria-label={t('click_to_view_queries')}>
{children}
</Link>
);
};
export default LogsSearchLink;

View File

@@ -24,7 +24,7 @@
width: 40px;
height: 40px;
margin-bottom: 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");
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;
}

View File

@@ -1,25 +0,0 @@
import React from 'react';
import PropTypes from 'prop-types';
import './PageTitle.css';
const PageTitle = ({
title, subtitle, children, containerClass,
}) => <div className="page-header">
<div className={containerClass}>
<h1 className="page-title pr-2">{title}</h1>
{children}
</div>
{subtitle && <div className="page-subtitle">
{subtitle}
</div>}
</div>;
PageTitle.propTypes = {
title: PropTypes.string.isRequired,
subtitle: PropTypes.string,
children: PropTypes.node,
containerClass: PropTypes.string,
};
export default PageTitle;

View File

@@ -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) => (
<div className="page-header">
<div className={containerClass}>
<h1 className="page-title pr-2">{title}</h1>
{children}
</div>
{subtitle && <div className="page-subtitle">{subtitle}</div>}
</div>
);
export default PageTitle;

View File

@@ -23,24 +23,25 @@
color: var(--gray300);
}
.ReactTable .-pagination input, .ReactTable .-pagination select {
.ReactTable .-pagination input,
.ReactTable .-pagination select {
color: var(--rt-nodata-color);
background-color: var(--rt-nodata-bgcolor);
}
[data-theme=dark] .ReactTable .rt-table::-webkit-scrollbar-track {
[data-theme='dark'] .ReactTable .rt-table::-webkit-scrollbar-track {
background-color: var(--card-bgcolor);
}
[data-theme=dark] .ReactTable .rt-table::-webkit-scrollbar-thumb {
[data-theme='dark'] .ReactTable .rt-table::-webkit-scrollbar-thumb {
background-color: #888888;
}
[data-theme=dark] .ReactTable .-pagination .-btn {
[data-theme='dark'] .ReactTable .-pagination .-btn {
filter: invert(1);
}
[data-theme=dark] .ReactTable .-pagination .-btn:disabled {
[data-theme='dark'] .ReactTable .-pagination .-btn:disabled {
opacity: 1;
}

View File

@@ -3,7 +3,7 @@
padding: 0 32px 2px 11px;
outline: 0;
border-color: var(--ctrl-select-bgcolor);
background-image: url("./svg/chevron-down.svg");
background-image: url("data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHdpZHRoPSIyNCIgaGVpZ2h0PSIyNCIgdmlld0JveD0iMCAwIDI0IDI0IiBmaWxsPSJub25lIiBzdHJva2U9IiM5YWEwYWMiCiAgICAgc3Ryb2tlLXdpZHRoPSIyIiBzdHJva2UtbGluZWNhcD0icm91bmQiIHN0cm9rZS1saW5lam9pbj0icm91bmQiIGNsYXNzPSJmZWF0aGVyIGZlYXRoZXItY2hldnJvbi1kb3duIj4KICAgIDxwb2x5bGluZSBwb2ludHM9IjYgOSAxMiAxNSAxOCA5Ij48L3BvbHlsaW5lPgo8L3N2Zz4K");
background-repeat: no-repeat;
background-position: right 9px center;
background-size: 17px 20px;
@@ -20,10 +20,14 @@
padding: 0 32px 2px 33px;
outline: 0;
border-color: var(--ctrl-select-bgcolor);
background-image: url("./svg/globe.svg"), url("./svg/chevron-down.svg");
background-image: url("data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHdpZHRoPSIyNCIgaGVpZ2h0PSIyNCIgdmlld0JveD0iMCAwIDI0IDI0IiBmaWxsPSJub25lIiBzdHJva2U9IiM5YWEwYWMiCiAgICAgc3Ryb2tlLXdpZHRoPSIyIiBzdHJva2UtbGluZWNhcD0icm91bmQiIHN0cm9rZS1saW5lam9pbj0icm91bmQiIGNsYXNzPSJmZWF0aGVyIGZlYXRoZXItZ2xvYmUiPgogICAgPGNpcmNsZSBjeD0iMTIiIGN5PSIxMiIgcj0iMTAiLz4KICAgIDxwYXRoIGQ9Ik0yIDEyaDIwTTEyIDJhMTUuMyAxNS4zIDAgMCAxIDQgMTAgMTUuMyAxNS4zIDAgMCAxLTQgMTAgMTUuMyAxNS4zIDAgMCAxLTQtMTAgMTUuMyAxNS4zIDAgMCAxIDQtMTB6Ii8+Cjwvc3ZnPgo="), url("data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHdpZHRoPSIyNCIgaGVpZ2h0PSIyNCIgdmlld0JveD0iMCAwIDI0IDI0IiBmaWxsPSJub25lIiBzdHJva2U9IiM5YWEwYWMiCiAgICAgc3Ryb2tlLXdpZHRoPSIyIiBzdHJva2UtbGluZWNhcD0icm91bmQiIHN0cm9rZS1saW5lam9pbj0icm91bmQiIGNsYXNzPSJmZWF0aGVyIGZlYXRoZXItY2hldnJvbi1kb3duIj4KICAgIDxwb2x5bGluZSBwb2ludHM9IjYgOSAxMiAxNSAxOCA5Ij48L3BvbHlsaW5lPgo8L3N2Zz4K");
background-repeat: no-repeat, no-repeat;
background-position: left 11px center, right 9px center;
background-size: 14px, 17px 20px;
background-position:
left 11px center,
right 9px center;
background-size:
14px,
17px 20px;
appearance: none;
cursor: pointer;
}
@@ -33,7 +37,7 @@
}
.basic-multi-select .select__control {
border: 1px solid var(--card-border-color);;
border: 1px solid var(--card-border-color);
border-radius: 3px;
background-color: var(--ctrl-bgcolor);
}
@@ -57,13 +61,13 @@
background-color: var(--ctrl-bgcolor);
}
[data-theme=dark] .basic-multi-select .select__option:hover,
[data-theme=dark] .basic-multi-select .select__option--is-focused,
[data-theme=dark] .basic-multi-select .select__option--is-focused:hover {
[data-theme='dark'] .basic-multi-select .select__option:hover,
[data-theme='dark'] .basic-multi-select .select__option--is-focused,
[data-theme='dark'] .basic-multi-select .select__option--is-focused:hover {
background-color: var(--ctrl-select-bgcolor);
color: var(--ctrl-dropdown-color);
}
[data-theme=dark] .select__multi-value__remove svg {
[data-theme='dark'] .select__multi-value__remove svg {
filter: invert(1);
}

View File

@@ -1,27 +0,0 @@
import React from 'react';
import PropTypes from 'prop-types';
import { withTranslation, Trans } from 'react-i18next';
import Card from './Card';
const Status = ({ message, buttonMessage, reloadPage }) => (
<div className="status">
<Card bodyType="card-body card-body--status">
<div className="h4 font-weight-light mb-4">
<Trans>{message}</Trans>
</div>
{buttonMessage
&& <button className="btn btn-success" onClick={reloadPage}>
<Trans>{buttonMessage}</Trans>
</button>}
</Card>
</div>
);
Status.propTypes = {
message: PropTypes.string.isRequired,
buttonMessage: PropTypes.string,
reloadPage: PropTypes.func,
};
export default withTranslation()(Status);

View File

@@ -0,0 +1,27 @@
import React from 'react';
import { withTranslation, Trans } from 'react-i18next';
import Card from './Card';
interface StatusProps {
message: string;
buttonMessage?: string;
reloadPage?: (...args: unknown[]) => unknown;
}
const Status = ({ message, buttonMessage, reloadPage }: StatusProps) => (
<div className="status">
<Card bodyType="card-body card-body--status">
<div className="h4 font-weight-light mb-4">
<Trans>{message}</Trans>
</div>
{buttonMessage && (
<button className="btn btn-success" onClick={reloadPage}>
<Trans>{buttonMessage}</Trans>
</button>
)}
</Card>
</div>
);
export default withTranslation()(Status);

View File

@@ -1,11 +1,15 @@
import React from 'react';
import PropTypes from 'prop-types';
import classnames from 'classnames';
import { useTranslation } from 'react-i18next';
const Tab = ({
activeTabLabel, label, title, onClick,
}) => {
interface TabProps {
activeTabLabel: string;
label: string;
onClick: (...args: unknown[]) => unknown;
title?: string;
}
const Tab = ({ activeTabLabel, label, title, onClick }: TabProps) => {
const [t] = useTranslation();
const handleClick = () => onClick(label);
@@ -15,10 +19,7 @@ const Tab = ({
});
return (
<div
className={tabClass}
onClick={handleClick}
>
<div className={tabClass} onClick={handleClick}>
<svg className="tab__icon">
<use xlinkHref={`#${label.toLowerCase()}`} />
</svg>
@@ -27,11 +28,4 @@ const Tab = ({
);
};
Tab.propTypes = {
activeTabLabel: PropTypes.string.isRequired,
label: PropTypes.string.isRequired,
onClick: PropTypes.func.isRequired,
title: PropTypes.string,
};
export default Tab;

File diff suppressed because one or more lines are too long

View File

@@ -40,7 +40,7 @@
opacity: 0.6;
}
[data-theme=dark] .tab__control {
[data-theme='dark'] .tab__control {
filter: invert(1);
}

View File

@@ -1,52 +0,0 @@
import React from 'react';
import PropTypes from 'prop-types';
import classnames from 'classnames';
import Tab from './Tab';
import './Tabs.css';
const Tabs = (props) => {
const {
tabs, controlClass, activeTabLabel, setActiveTabLabel, children: activeTab,
} = props;
const onClickTabControl = (tabLabel) => setActiveTabLabel(tabLabel);
const getControlClass = classnames({
tabs__controls: true,
[`tabs__controls--${controlClass}`]: controlClass,
});
return (
<div className="tabs">
<div className={getControlClass}>
{Object.values(tabs)
.map((props) => {
// eslint-disable-next-line react/prop-types
const { title, label = title } = props;
return (
<Tab
key={label}
label={label}
title={title}
activeTabLabel={activeTabLabel}
onClick={onClickTabControl}
/>
);
})}
</div>
<div className="tabs__content">
{activeTab}
</div>
</div>
);
};
Tabs.propTypes = {
controlClass: PropTypes.string,
tabs: PropTypes.object.isRequired,
activeTabLabel: PropTypes.string.isRequired,
setActiveTabLabel: PropTypes.func.isRequired,
children: PropTypes.element.isRequired,
};
export default Tabs;

View File

@@ -0,0 +1,48 @@
import React from 'react';
import classnames from 'classnames';
import Tab from './Tab';
import './Tabs.css';
interface TabsProps {
controlClass?: string;
tabs: object;
activeTabLabel: string;
setActiveTabLabel: (...args: unknown[]) => unknown;
children: React.ReactElement;
}
const Tabs = (props: TabsProps) => {
const { tabs, controlClass, activeTabLabel, setActiveTabLabel, children: activeTab } = props;
const onClickTabControl = (tabLabel: any) => setActiveTabLabel(tabLabel);
const getControlClass = classnames({
tabs__controls: true,
[`tabs__controls--${controlClass}`]: controlClass,
});
return (
<div className="tabs">
<div className={getControlClass}>
{Object.values(tabs).map((props: any) => {
// eslint-disable-next-line react/prop-types
const { title, label = title } = props;
return (
<Tab
key={label}
label={label}
title={title}
activeTabLabel={activeTabLabel}
onClick={onClickTabControl}
/>
);
})}
</div>
<div className="tabs__content">{activeTab}</div>
</div>
);
};
export default Tabs;

View File

@@ -4,7 +4,7 @@
box-shadow: 1px 1px 6px rgba(0, 0, 0, 0.2);
}
[data-theme=dark] .tooltip-container {
[data-theme='dark'] .tooltip-container {
background-color: var(--ctrl-select-bgcolor);
color: var(--mcolor);
}

View File

@@ -1,105 +0,0 @@
import React from 'react';
import TooltipTrigger from 'react-popper-tooltip';
import propTypes from 'prop-types';
import { useTranslation } from 'react-i18next';
import {
HIDE_TOOLTIP_DELAY,
MEDIUM_SCREEN_SIZE,
SHOW_TOOLTIP_DELAY,
} from '../../helpers/constants';
import 'react-popper-tooltip/dist/styles.css';
import './Tooltip.css';
const Tooltip = ({
children,
content,
triggerClass = 'tooltip-custom__trigger',
className = 'tooltip-container',
placement = 'bottom',
trigger = 'hover',
delayShow = SHOW_TOOLTIP_DELAY,
delayHide = HIDE_TOOLTIP_DELAY,
onVisibilityChange,
defaultTooltipShown,
}) => {
const { t } = useTranslation();
const touchEventsAvailable = 'ontouchstart' in window;
let triggerValue = trigger;
let delayHideValue = delayHide;
let delayShowValue = delayShow;
if (window.matchMedia(`(max-width: ${MEDIUM_SCREEN_SIZE}px)`).matches || touchEventsAvailable) {
triggerValue = 'click';
delayHideValue = 0;
delayShowValue = 0;
}
const renderTooltip = ({ tooltipRef, getTooltipProps }) => (
<div
{...getTooltipProps({
ref: tooltipRef,
className,
})}
>
{typeof content === 'string' ? t(content) : content}
</div>
);
const renderTrigger = ({ getTriggerProps, triggerRef }) => (
<span
{...getTriggerProps({
ref: triggerRef,
className: triggerClass,
})}
>
{children}
</span>
);
renderTooltip.propTypes = {
tooltipRef: propTypes.object,
getTooltipProps: propTypes.func,
};
renderTrigger.propTypes = {
triggerRef: propTypes.object,
getTriggerProps: propTypes.func,
};
return (
<TooltipTrigger
placement={placement}
trigger={triggerValue}
delayHide={delayHideValue}
delayShow={delayShowValue}
tooltip={renderTooltip}
onVisibilityChange={onVisibilityChange}
defaultTooltipShown={defaultTooltipShown}
>
{renderTrigger}
</TooltipTrigger>
);
};
Tooltip.propTypes = {
children: propTypes.element.isRequired,
content: propTypes.oneOfType(
[
propTypes.string,
propTypes.element,
propTypes.arrayOf(propTypes.element),
],
).isRequired,
placement: propTypes.string,
trigger: propTypes.string,
delayHide: propTypes.number,
delayShow: propTypes.number,
className: propTypes.string,
triggerClass: propTypes.string,
onVisibilityChange: propTypes.func,
defaultTooltipShown: propTypes.bool,
};
export default Tooltip;

View File

@@ -0,0 +1,92 @@
import React from 'react';
import PopperJS from 'popper.js';
import TooltipTrigger, { TriggerTypes } from 'react-popper-tooltip';
import { useTranslation } from 'react-i18next';
import { HIDE_TOOLTIP_DELAY, MEDIUM_SCREEN_SIZE, SHOW_TOOLTIP_DELAY } from '../../helpers/constants';
import 'react-popper-tooltip/dist/styles.css';
import './Tooltip.css';
interface TooltipProps {
children: React.ReactElement;
content: string | React.ReactElement | React.ReactElement[];
placement?: PopperJS.Placement;
trigger?: TriggerTypes;
delayHide?: number;
delayShow?: number;
className?: string;
triggerClass?: string;
onVisibilityChange?: (...args: unknown[]) => unknown;
defaultTooltipShown?: boolean;
}
interface renderTooltipProps {
tooltipRef?: object;
getTooltipProps?: (...args: unknown[]) => Record<any, any>;
}
interface renderTriggerProps {
triggerRef?: object;
getTriggerProps?: (...args: unknown[]) => Record<any, any>;
}
const Tooltip = ({
children,
content,
triggerClass = 'tooltip-custom__trigger',
className = 'tooltip-container',
placement = 'bottom',
trigger = 'hover',
delayShow = SHOW_TOOLTIP_DELAY,
delayHide = HIDE_TOOLTIP_DELAY,
onVisibilityChange,
defaultTooltipShown,
}: TooltipProps) => {
const { t } = useTranslation();
const touchEventsAvailable = 'ontouchstart' in window;
let triggerValue = trigger;
let delayHideValue = delayHide;
let delayShowValue = delayShow;
if (window.matchMedia(`(max-width: ${MEDIUM_SCREEN_SIZE}px)`).matches || touchEventsAvailable) {
triggerValue = 'click';
delayHideValue = 0;
delayShowValue = 0;
}
const renderTooltip = ({ tooltipRef, getTooltipProps }: renderTooltipProps) => (
<div
{...getTooltipProps({
ref: tooltipRef,
className,
})}>
{typeof content === 'string' ? t(content) : content}
</div>
);
const renderTrigger = ({ getTriggerProps, triggerRef }: renderTriggerProps) => (
<span
{...getTriggerProps({
ref: triggerRef,
className: triggerClass,
})}>
{children}
</span>
);
return (
<TooltipTrigger
placement={placement}
trigger={triggerValue}
delayHide={delayHideValue}
delayShow={delayShowValue}
tooltip={renderTooltip}
onVisibilityChange={onVisibilityChange}
defaultTooltipShown={defaultTooltipShown}>
{renderTrigger}
</TooltipTrigger>
);
};
export default Tooltip;

View File

@@ -1,19 +0,0 @@
import React from 'react';
import PropTypes from 'prop-types';
import './Topline.css';
const Topline = (props) => (
<div className={`alert alert-${props.type} topline`}>
<div className="container">
{props.children}
</div>
</div>
);
Topline.propTypes = {
children: PropTypes.node.isRequired,
type: PropTypes.string.isRequired,
};
export default Topline;

View File

@@ -0,0 +1,16 @@
import React from 'react';
import './Topline.css';
interface ToplineProps {
children: React.ReactNode;
type: string;
}
const Topline = (props: ToplineProps) => (
<div className={`alert alert-${props.type} topline`}>
<div className="container">{props.children}</div>
</div>
);
export default Topline;

View File

@@ -3,9 +3,10 @@ import { Trans } from 'react-i18next';
import classnames from 'classnames';
import { useSelector } from 'react-redux';
import './Overlay.css';
import { RootState } from '../../initialState';
const UpdateOverlay = () => {
const processingUpdate = useSelector((state) => state.dashboard.processingUpdate);
const processingUpdate = useSelector((state: RootState) => state.dashboard.processingUpdate);
const overlayClass = classnames('overlay', {
'overlay--visible': processingUpdate,
});
@@ -13,6 +14,7 @@ const UpdateOverlay = () => {
return (
<div className={overlayClass}>
<div className="overlay__loading"></div>
<Trans>processing_update</Trans>
</div>
);

View File

@@ -1,59 +0,0 @@
import React from 'react';
import { Trans } from 'react-i18next';
import { shallowEqual, useDispatch, useSelector } from 'react-redux';
import Topline from './Topline';
import { getUpdate } from '../../actions';
import { MANUAL_UPDATE_LINK } from '../../helpers/constants';
const UpdateTopline = () => {
const {
announcementUrl,
newVersion,
canAutoUpdate,
processingUpdate,
} = useSelector((state) => state.dashboard, shallowEqual);
const dispatch = useDispatch();
const handleUpdate = () => {
dispatch(getUpdate());
};
return <Topline type="info">
<>
<Trans
values={{ version: newVersion }}
components={[
<a href={announcementUrl} target="_blank" rel="noopener noreferrer" key="0">
Click here
</a>,
]}
>
update_announcement
</Trans>
&nbsp;
{canAutoUpdate ? (
<button
type="button"
className="btn btn-sm btn-primary ml-3"
onClick={handleUpdate}
disabled={processingUpdate}
>
<Trans>update_now</Trans>
</button>
) : (
<Trans components={{
a: (
<a href={MANUAL_UPDATE_LINK} target="_blank" rel="noopener noreferrer" key="0">
Link
</a>
),
}}>
manual_update
</Trans>
)}
</>
</Topline>;
};
export default UpdateTopline;

View File

@@ -0,0 +1,60 @@
import React from 'react';
import { Trans } from 'react-i18next';
import { shallowEqual, useDispatch, useSelector } from 'react-redux';
import Topline from './Topline';
import { getUpdate } from '../../actions';
import { MANUAL_UPDATE_LINK } from '../../helpers/constants';
import { RootState } from '../../initialState';
const UpdateTopline = () => {
const { announcementUrl, newVersion, canAutoUpdate, processingUpdate } = useSelector(
(state: RootState) => state.dashboard,
shallowEqual,
);
const dispatch = useDispatch();
const handleUpdate = () => {
dispatch(getUpdate());
};
return (
<Topline type="info">
<>
<Trans
values={{ version: newVersion }}
components={[
<a href={announcementUrl} target="_blank" rel="noopener noreferrer" key="0">
Click here
</a>,
]}>
update_announcement
</Trans>
&nbsp;
{canAutoUpdate ? (
<button
type="button"
className="btn btn-sm btn-primary ml-3"
onClick={handleUpdate}
disabled={processingUpdate}>
<Trans>update_now</Trans>
</button>
) : (
<Trans
components={{
a: (
<a href={MANUAL_UPDATE_LINK} target="_blank" rel="noopener noreferrer" key="0">
Link
</a>
),
}}>
manual_update
</Trans>
)}
</>
</Topline>
);
};
export default UpdateTopline;

View File

@@ -1,6 +1,5 @@
.version {
font-size: 0.80rem;
font-size: 0.8rem;
}
@media screen and (min-width: 1280px) {

View File

@@ -1,51 +0,0 @@
import React from 'react';
import { Trans, useTranslation } from 'react-i18next';
import { shallowEqual, useDispatch, useSelector } from 'react-redux';
import { getVersion } from '../../actions';
import './Version.css';
const Version = () => {
const dispatch = useDispatch();
const { t } = useTranslation();
const {
dnsVersion,
processingVersion,
checkUpdateFlag,
} = useSelector((state) => state?.dashboard ?? {}, shallowEqual);
const {
dnsVersion: installDnsVersion,
} = useSelector((state) => state?.install ?? {}, shallowEqual);
const version = dnsVersion || installDnsVersion;
const onClick = () => {
dispatch(getVersion(true));
};
return (
<div className="version">
<div className="version__text">
{version && (
<>
<Trans>version</Trans>:&nbsp;
<span className="version__value" title={version}>{version}</span>
</>
)}
{checkUpdateFlag && <button
type="button"
className="btn btn-icon btn-icon-sm btn-outline-primary btn-sm ml-2"
onClick={onClick}
disabled={processingVersion}
title={t('check_updates_now')}
>
<svg className="icons icon12">
<use xlinkHref="#refresh" />
</svg>
</button>}
</div>
</div>
);
};
export default Version;

View File

@@ -0,0 +1,55 @@
import React from 'react';
import { Trans, useTranslation } from 'react-i18next';
import { shallowEqual, useDispatch, useSelector } from 'react-redux';
import { getVersion } from '../../actions';
import './Version.css';
import { RootState } from '../../initialState';
const Version = () => {
const dispatch = useDispatch();
const { t } = useTranslation();
const dashboard = useSelector((state: RootState) => state.dashboard, shallowEqual);
const install = useSelector((state: RootState) => state.install, shallowEqual);
if (!dashboard || !install) {
return null;
}
const { dnsVersion, processingVersion, checkUpdateFlag } = dashboard;
const version = dnsVersion || install?.dnsVersion;
const onClick = () => {
dispatch(getVersion(true));
};
return (
<div className="version">
<div className="version__text">
{version && (
<>
<Trans>version</Trans>:&nbsp;
<span className="version__value" title={version}>
{version}
</span>
</>
)}
{checkUpdateFlag && (
<button
type="button"
className="btn btn-icon btn-icon-sm btn-outline-primary btn-sm ml-2"
onClick={onClick}
disabled={processingVersion}
title={t('check_updates_now')}>
<svg className="icons icon12">
<use xlinkHref="#refresh" />
</svg>
</button>
)}
</div>
</div>
);
};
export default Version;

View File

@@ -1,8 +0,0 @@
<svg xmlns="http://www.w3.org/2000/svg" width="164" height="41" viewBox="0 0 164 41">
<g fill-rule="evenodd">
<path d="M129.984 22l-1.162-2.945h-5.792L121.931 22H118l6.277-15h3.509L134 22h-4.016zm-4.016-10.996l-1.902 5.149h3.762l-1.86-5.149zM117 16.1c0 .88-.153 1.682-.46 2.404a5.223 5.223 0 0 1-1.318 1.857c-.57.516-1.26.918-2.066 1.207-.807.289-1.703.433-2.688.433-1 0-1.9-.144-2.699-.433-.8-.29-1.477-.691-2.034-1.207a5.232 5.232 0 0 1-1.285-1.857c-.3-.722-.45-1.524-.45-2.404V7h3.64v8.81c0 .4.054.777.161 1.135.108.358.272.677.493.96.221.281.514.505.878.67.364.165.803.248 1.317.248.514 0 .953-.083 1.317-.248.365-.165.66-.389.89-.67.228-.283.392-.602.492-.96.1-.358.15-.736.15-1.135V7H117v9.099zm-16 4.673c-.733.362-1.59.658-2.57.886-.98.228-2.047.342-3.203.342-1.199 0-2.302-.181-3.31-.544-1.008-.362-1.875-.872-2.601-1.53a6.977 6.977 0 0 1-1.703-2.366c-.409-.92-.613-1.943-.613-3.07 0-1.141.208-2.175.624-3.1a6.903 6.903 0 0 1 1.723-2.367 7.71 7.71 0 0 1 2.58-1.5C92.914 7.174 93.98 7 95.121 7c1.184 0 2.284.171 3.299.513 1.015.343 1.84.802 2.474 1.38l-2.284 2.476c-.352-.39-.817-.708-1.395-.956-.579-.249-1.234-.373-1.967-.373-.635 0-1.22.111-1.756.332a4.23 4.23 0 0 0-1.395.927 4.178 4.178 0 0 0-.92 1.41 4.734 4.734 0 0 0-.328 1.78c0 .659.099 1.263.296 1.813.197.55.49 1.024.878 1.42.387.395.867.704 1.438.926.57.221 1.223.332 1.956.332.423 0 .825-.03 1.205-.09.381-.061.733-.158 1.058-.293V16h-2.855v-2.779H101v7.55zm63-6.314c0 1.313-.244 2.447-.73 3.4a6.855 6.855 0 0 1-1.928 2.352 8.035 8.035 0 0 1-2.7 1.356 10.94 10.94 0 0 1-3.05.434H150V7h5.422c1.06 0 2.104.124 3.135.37a7.866 7.866 0 0 1 2.753 1.23c.805.572 1.454 1.338 1.949 2.298.494.96.741 2.147.741 3.56zm-3.77 0c0-.848-.138-1.55-.413-2.108a3.549 3.549 0 0 0-1.101-1.335 4.405 4.405 0 0 0-1.568-.71 7.7 7.7 0 0 0-1.81-.212h-1.8v8.771h1.715c.65 0 1.274-.074 1.874-.222a4.43 4.43 0 0 0 1.589-.731 3.62 3.62 0 0 0 1.1-1.356c.276-.565.414-1.264.414-2.097zm-75.23 0c0 1.313-.244 2.447-.73 3.4a6.855 6.855 0 0 1-1.928 2.352 8.035 8.035 0 0 1-2.7 1.356 10.94 10.94 0 0 1-3.05.434H71V7h5.422c1.06 0 2.104.124 3.135.37A7.866 7.866 0 0 1 82.31 8.6c.805.572 1.454 1.338 1.949 2.298.494.96.741 2.147.741 3.56zm-3.77 0c0-.848-.138-1.55-.413-2.108a3.549 3.549 0 0 0-1.101-1.335 4.405 4.405 0 0 0-1.568-.71 7.7 7.7 0 0 0-1.81-.212h-1.8v8.771h1.715c.65 0 1.274-.074 1.874-.222a4.43 4.43 0 0 0 1.589-.731 3.62 3.62 0 0 0 1.1-1.356c.276-.565.414-1.264.414-2.097zM65.984 22l-1.162-2.945H59.03L57.931 22H54l6.277-15h3.509L70 22h-4.016zm-4.016-10.996l-1.902 5.149h3.762l-1.86-5.149zM143.855 22l-3.171-5.953h-1.202V22H136V7h5.596c.705 0 1.392.074 2.062.222.67.149 1.271.4 1.803.753a3.9 3.9 0 0 1 1.275 1.398c.318.579.476 1.3.476 2.16 0 1.018-.269 1.872-.808 2.564-.539.693-1.285 1.187-2.238 1.484L148 22h-4.145zm-.145-10.403c0-.353-.073-.639-.218-.858a1.502 1.502 0 0 0-.56-.508 2.393 2.393 0 0 0-.766-.244 5.535 5.535 0 0 0-.819-.063h-1.886v3.495h1.679c.29 0 .587-.024.891-.074.304-.05.58-.137.83-.264.248-.128.452-.311.61-.551.16-.24.239-.551.239-.933zM55 37.851v-8.702h.951v3.866h4.866V29.15h.952v8.702h-.952v-3.916h-4.866v3.916H55zM68.068 38c-2.565 0-4.288-2.076-4.288-4.5 0-2.4 1.747-4.5 4.312-4.5 2.565 0 4.288 2.076 4.288 4.5 0 2.4-1.747 4.5-4.312 4.5zm.024-.907c1.927 0 3.3-1.592 3.3-3.593 0-1.977-1.397-3.593-3.324-3.593-1.927 0-3.3 1.592-3.3 3.593 0 1.977 1.397 3.593 3.324 3.593zm6.3.758v-8.702h.963l3.07 4.749 3.072-4.749h.964v8.702h-.952v-7.049l-3.071 4.662h-.048l-3.071-4.65v7.037h-.928zm10.453 0v-8.702h6.095v.895h-5.143v2.971h4.6v.895h-4.6v3.046H91v.895h-6.155z"/>
<path fill-rule="nonzero"
d="M2.831 14.045c.775 4.287 2.266 8.333 4.685 12.143 2.958 4.659 7.21 8.797 12.984 12.319 5.774-3.522 10.026-7.66 12.984-12.319 2.42-3.81 3.91-7.856 4.685-12.143.489-2.706.644-4.844.672-8.003C33.368 3.522 26.636 2.14 20.5 2.14c-6.137 0-12.869 1.381-18.341 3.9.028 3.16.183 5.298.672 8.004zM20.5 0C26.908 0 34.637 1.47 41 4.706c0 6.988.087 24.398-20.5 36.294C-.088 29.104 0 11.694 0 4.706 6.363 1.47 14.092 0 20.5 0z"/>
<path d="M20.234 27L33 11.344c-.935-.682-1.756-.2-2.208.172l-.016.001-10.644 10.076-4.01-4.392c-1.913-2.011-4.514-.477-5.122-.072L20.234 27"/>
</g>
</svg>

Before

Width:  |  Height:  |  Size: 4.1 KiB

View File

@@ -0,0 +1,22 @@
import React, { memo } from 'react';
type Props = {
className?: string;
};
export const Logo = memo(({ className }: Props) => {
return (
<svg xmlns="http://www.w3.org/2000/svg" width="164" height="41" viewBox="0 0 164 41" className={className}>
<g fillRule="evenodd">
<path d="M129.984 22l-1.162-2.945h-5.792L121.931 22H118l6.277-15h3.509L134 22h-4.016zm-4.016-10.996l-1.902 5.149h3.762l-1.86-5.149zM117 16.1c0 .88-.153 1.682-.46 2.404a5.223 5.223 0 0 1-1.318 1.857c-.57.516-1.26.918-2.066 1.207-.807.289-1.703.433-2.688.433-1 0-1.9-.144-2.699-.433-.8-.29-1.477-.691-2.034-1.207a5.232 5.232 0 0 1-1.285-1.857c-.3-.722-.45-1.524-.45-2.404V7h3.64v8.81c0 .4.054.777.161 1.135.108.358.272.677.493.96.221.281.514.505.878.67.364.165.803.248 1.317.248.514 0 .953-.083 1.317-.248.365-.165.66-.389.89-.67.228-.283.392-.602.492-.96.1-.358.15-.736.15-1.135V7H117v9.099zm-16 4.673c-.733.362-1.59.658-2.57.886-.98.228-2.047.342-3.203.342-1.199 0-2.302-.181-3.31-.544-1.008-.362-1.875-.872-2.601-1.53a6.977 6.977 0 0 1-1.703-2.366c-.409-.92-.613-1.943-.613-3.07 0-1.141.208-2.175.624-3.1a6.903 6.903 0 0 1 1.723-2.367 7.71 7.71 0 0 1 2.58-1.5C92.914 7.174 93.98 7 95.121 7c1.184 0 2.284.171 3.299.513 1.015.343 1.84.802 2.474 1.38l-2.284 2.476c-.352-.39-.817-.708-1.395-.956-.579-.249-1.234-.373-1.967-.373-.635 0-1.22.111-1.756.332a4.23 4.23 0 0 0-1.395.927 4.178 4.178 0 0 0-.92 1.41 4.734 4.734 0 0 0-.328 1.78c0 .659.099 1.263.296 1.813.197.55.49 1.024.878 1.42.387.395.867.704 1.438.926.57.221 1.223.332 1.956.332.423 0 .825-.03 1.205-.09.381-.061.733-.158 1.058-.293V16h-2.855v-2.779H101v7.55zm63-6.314c0 1.313-.244 2.447-.73 3.4a6.855 6.855 0 0 1-1.928 2.352 8.035 8.035 0 0 1-2.7 1.356 10.94 10.94 0 0 1-3.05.434H150V7h5.422c1.06 0 2.104.124 3.135.37a7.866 7.866 0 0 1 2.753 1.23c.805.572 1.454 1.338 1.949 2.298.494.96.741 2.147.741 3.56zm-3.77 0c0-.848-.138-1.55-.413-2.108a3.549 3.549 0 0 0-1.101-1.335 4.405 4.405 0 0 0-1.568-.71 7.7 7.7 0 0 0-1.81-.212h-1.8v8.771h1.715c.65 0 1.274-.074 1.874-.222a4.43 4.43 0 0 0 1.589-.731 3.62 3.62 0 0 0 1.1-1.356c.276-.565.414-1.264.414-2.097zm-75.23 0c0 1.313-.244 2.447-.73 3.4a6.855 6.855 0 0 1-1.928 2.352 8.035 8.035 0 0 1-2.7 1.356 10.94 10.94 0 0 1-3.05.434H71V7h5.422c1.06 0 2.104.124 3.135.37A7.866 7.866 0 0 1 82.31 8.6c.805.572 1.454 1.338 1.949 2.298.494.96.741 2.147.741 3.56zm-3.77 0c0-.848-.138-1.55-.413-2.108a3.549 3.549 0 0 0-1.101-1.335 4.405 4.405 0 0 0-1.568-.71 7.7 7.7 0 0 0-1.81-.212h-1.8v8.771h1.715c.65 0 1.274-.074 1.874-.222a4.43 4.43 0 0 0 1.589-.731 3.62 3.62 0 0 0 1.1-1.356c.276-.565.414-1.264.414-2.097zM65.984 22l-1.162-2.945H59.03L57.931 22H54l6.277-15h3.509L70 22h-4.016zm-4.016-10.996l-1.902 5.149h3.762l-1.86-5.149zM143.855 22l-3.171-5.953h-1.202V22H136V7h5.596c.705 0 1.392.074 2.062.222.67.149 1.271.4 1.803.753a3.9 3.9 0 0 1 1.275 1.398c.318.579.476 1.3.476 2.16 0 1.018-.269 1.872-.808 2.564-.539.693-1.285 1.187-2.238 1.484L148 22h-4.145zm-.145-10.403c0-.353-.073-.639-.218-.858a1.502 1.502 0 0 0-.56-.508 2.393 2.393 0 0 0-.766-.244 5.535 5.535 0 0 0-.819-.063h-1.886v3.495h1.679c.29 0 .587-.024.891-.074.304-.05.58-.137.83-.264.248-.128.452-.311.61-.551.16-.24.239-.551.239-.933zM55 37.851v-8.702h.951v3.866h4.866V29.15h.952v8.702h-.952v-3.916h-4.866v3.916H55zM68.068 38c-2.565 0-4.288-2.076-4.288-4.5 0-2.4 1.747-4.5 4.312-4.5 2.565 0 4.288 2.076 4.288 4.5 0 2.4-1.747 4.5-4.312 4.5zm.024-.907c1.927 0 3.3-1.592 3.3-3.593 0-1.977-1.397-3.593-3.324-3.593-1.927 0-3.3 1.592-3.3 3.593 0 1.977 1.397 3.593 3.324 3.593zm6.3.758v-8.702h.963l3.07 4.749 3.072-4.749h.964v8.702h-.952v-7.049l-3.071 4.662h-.048l-3.071-4.65v7.037h-.928zm10.453 0v-8.702h6.095v.895h-5.143v2.971h4.6v.895h-4.6v3.046H91v.895h-6.155z" />
<path
fillRule="nonzero"
d="M2.831 14.045c.775 4.287 2.266 8.333 4.685 12.143 2.958 4.659 7.21 8.797 12.984 12.319 5.774-3.522 10.026-7.66 12.984-12.319 2.42-3.81 3.91-7.856 4.685-12.143.489-2.706.644-4.844.672-8.003C33.368 3.522 26.636 2.14 20.5 2.14c-6.137 0-12.869 1.381-18.341 3.9.028 3.16.183 5.298.672 8.004zM20.5 0C26.908 0 34.637 1.47 41 4.706c0 6.988.087 24.398-20.5 36.294C-.088 29.104 0 11.694 0 4.706 6.363 1.47 14.092 0 20.5 0z"
/>
<path d="M20.234 27L33 11.344c-.935-.682-1.756-.2-2.208.172l-.016.001-10.644 10.076-4.01-4.392c-1.913-2.011-4.514-.477-5.122-.072L20.234 27" />
</g>
</svg>
);
});
Logo.displayName = 'Logo';