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:4c89cf7201085d59a6Author: Igor Lobanov <bniwredyc@gmail.com> Date: Mon Jun 10 17:22:20 2024 +0200 merge commit4c89cf7209Author: Ildar Kamalov <ik@adguard.com> Date: Thu Jun 6 13:27:18 2024 +0300 remove install from initial state commitb943f2011fAuthor: Igor Lobanov <bniwredyc@gmail.com> Date: Wed Jun 5 23:10:55 2024 +0200 frontend production build fix commitcd1be2d66dAuthor: Igor Lobanov <bniwredyc@gmail.com> Date: Wed Jun 5 20:23:14 2024 +0200 production build quickfix commit7b8ac01fc2Author: Ainar Garipov <A.Garipov@AdGuard.COM> Date: Wed Jun 5 19:57:31 2024 +0300 all: upd node docker commit02afed66d5Author: Igor Lobanov <bniwredyc@gmail.com> Date: Wed Jun 5 18:23:12 2024 +0200 changelog fixes commit9c0f736f0cMerge:62c4fbf1ee04775c4fAuthor: Igor Lobanov <bniwredyc@gmail.com> Date: Wed Jun 5 18:18:29 2024 +0200 merge commit62c4fbf1e3Author: Igor Lobanov <bniwredyc@gmail.com> Date: Wed Jun 5 16:22:22 2024 +0200 empty line in changelog commit76b1e44a93Author: Igor Lobanov <bniwredyc@gmail.com> Date: Wed Jun 5 16:20:37 2024 +0200 changelog commitf783e90040Author: Igor Lobanov <bniwredyc@gmail.com> Date: Wed Jun 5 16:19:13 2024 +0200 filters.js -> filters.ts commit3d4ce6554cAuthor: Igor Lobanov <bniwredyc@gmail.com> Date: Wed Jun 5 16:18:03 2024 +0200 generated file removed commite35ba58f2aAuthor: Igor Lobanov <bniwredyc@gmail.com> Date: Wed Jun 5 15:45:21 2024 +0200 rollback unwanted changes commit1f30d4216dAuthor: Igor Lobanov <bniwredyc@gmail.com> Date: Wed Jun 5 15:27:36 2024 +0200 review fix commit6cd4e44f07Author: Igor Lobanov <bniwredyc@gmail.com> Date: Wed Jun 5 11:55:39 2024 +0200 missing generated file restoresd commit2ab738b303Author: 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:
@@ -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;
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
33
client/src/components/ui/Card.tsx
Normal file
33
client/src/components/ui/Card.tsx
Normal 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;
|
||||
@@ -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;
|
||||
@@ -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;
|
||||
@@ -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 {
|
||||
|
||||
@@ -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);
|
||||
59
client/src/components/ui/Checkbox.tsx
Normal file
59
client/src/components/ui/Checkbox.tsx
Normal 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);
|
||||
@@ -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));
|
||||
@@ -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>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
|
||||
@@ -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')} © {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')} © {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>
|
||||
@@ -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;
|
||||
385
client/src/components/ui/Guide/Guide.tsx
Normal file
385
client/src/components/ui/Guide/Guide.tsx
Normal 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;
|
||||
@@ -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
485
client/src/components/ui/Icons.tsx
Normal file
485
client/src/components/ui/Icons.tsx
Normal file
File diff suppressed because one or more lines are too long
@@ -15,6 +15,6 @@
|
||||
color: var(--black);
|
||||
}
|
||||
|
||||
.card-chart-bg path[d^="M0,32"] {
|
||||
.card-chart-bg path[d^='M0,32'] {
|
||||
transform: translateY(32px);
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
85
client/src/components/ui/Line.tsx
Normal file
85
client/src/components/ui/Line.tsx
Normal 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;
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
@@ -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;
|
||||
36
client/src/components/ui/LogsSearchLink.tsx
Normal file
36
client/src/components/ui/LogsSearchLink.tsx
Normal 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;
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
23
client/src/components/ui/PageTitle.tsx
Normal file
23
client/src/components/ui/PageTitle.tsx
Normal 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;
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
27
client/src/components/ui/Status.tsx
Normal file
27
client/src/components/ui/Status.tsx
Normal 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);
|
||||
@@ -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
@@ -40,7 +40,7 @@
|
||||
opacity: 0.6;
|
||||
}
|
||||
|
||||
[data-theme=dark] .tab__control {
|
||||
[data-theme='dark'] .tab__control {
|
||||
filter: invert(1);
|
||||
}
|
||||
|
||||
|
||||
@@ -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;
|
||||
48
client/src/components/ui/Tabs.tsx
Normal file
48
client/src/components/ui/Tabs.tsx
Normal 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;
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
92
client/src/components/ui/Tooltip.tsx
Normal file
92
client/src/components/ui/Tooltip.tsx
Normal 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;
|
||||
@@ -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;
|
||||
16
client/src/components/ui/Topline.tsx
Normal file
16
client/src/components/ui/Topline.tsx
Normal 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;
|
||||
@@ -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>
|
||||
);
|
||||
@@ -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>
|
||||
|
||||
{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;
|
||||
60
client/src/components/ui/UpdateTopline.tsx
Normal file
60
client/src/components/ui/UpdateTopline.tsx
Normal 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>
|
||||
|
||||
{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;
|
||||
@@ -1,6 +1,5 @@
|
||||
|
||||
.version {
|
||||
font-size: 0.80rem;
|
||||
font-size: 0.8rem;
|
||||
}
|
||||
|
||||
@media screen and (min-width: 1280px) {
|
||||
|
||||
@@ -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>:
|
||||
<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;
|
||||
55
client/src/components/ui/Version.tsx
Normal file
55
client/src/components/ui/Version.tsx
Normal 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>:
|
||||
<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;
|
||||
@@ -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 |
22
client/src/components/ui/svg/logo.tsx
Normal file
22
client/src/components/ui/svg/logo.tsx
Normal 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';
|
||||
Reference in New Issue
Block a user